├── data └── put_all_data_here.txt ├── .gitignore ├── requirements.txt ├── code ├── Makefile ├── proc_2_compute_ssd_selected_peaks.py ├── proc_3_calculate_bursts_ssd.py ├── proc_1_calculate_spectral_param_electrodes.py ├── fig_5a_variability_plot_on_3d_surface.py ├── fig_6a_plot_waveshape_on_3dbrain.py ├── proc_4_calculate_spectral_param_ssd.py ├── proc_0_readin_to_mne.py ├── fig_7_noise_cleaning.py ├── fig_5b_variability_combine_in_plot.py ├── fig_6b_example_waveform_shape.py ├── fig_2_example_ecog.py ├── fig_S1_example_ecog_car_bipolar.py ├── fig_S2_example_motor_task.py ├── ssd.py ├── fig_4_spatial_spread.py ├── fig_3_example_seeg.py └── helper.py ├── LICENSE ├── csv ├── retained_components.csv └── selected_datasets.csv └── README.md /data/put_all_data_here.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data/motor_basic 2 | data/faces_basic 3 | data/fixation_pwrlaw 4 | code/__pycache__ 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.19.5 2 | pandas==1.2.0 3 | scipy==1.6.0 4 | mne==0.20.4 5 | fooof==1.0.0 6 | bycycle==0.1.3 7 | pyvista==0.29.1 8 | -------------------------------------------------------------------------------- /code/Makefile: -------------------------------------------------------------------------------- 1 | processing: 2 | @echo "Processing all participants..." 3 | python3 proc_0_readin_to_mne.py 4 | python3 proc_1_calculate_spectral_param_electrodes.py 5 | python3 proc_2_compute_ssd_selected_peaks.py 6 | python3 proc_3_calculate_bursts_ssd.py 7 | python3 proc_4_calculate_spectral_param_ssd.py 8 | 9 | figures: 10 | @echo "Generating figures..." 11 | ls fig_*.py|xargs -n 1 -P 2 python3 12 | 13 | all: 14 | processing figures 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Natalie Schaworonkow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /code/proc_2_compute_ssd_selected_peaks.py: -------------------------------------------------------------------------------- 1 | """ This script computes spatial filters and patterns. 2 | This procedure is done for all participants across selected peak 3 | frequencies. The filters and patterns are saved as numpy arrays. 4 | """ 5 | import mne 6 | import os 7 | import numpy as np 8 | import helper 9 | import ssd 10 | import pandas as pd 11 | 12 | df = pd.read_csv("../csv/selected_datasets.csv", index_col=False) 13 | df = df[df.is_rest] 14 | 15 | participants = df.participant 16 | experiments = df.experiment 17 | df = df.set_index("participant") 18 | 19 | results_dir = "../results/ssd/" 20 | os.makedirs(results_dir, exist_ok=True) 21 | 22 | 23 | for participant, experiment in list(zip(participants, experiments)): 24 | 25 | participant_id = "%s_%s" % (participant, experiment) 26 | print(participant_id) 27 | 28 | # get individual peaks 29 | peaks, bin_width1 = helper.get_participant_peaks( 30 | participant, experiment, return_width=True 31 | ) 32 | 33 | # load raw-file 34 | file_name = "../working/%s_raw.fif" % participant_id 35 | raw = mne.io.read_raw_fif(file_name, preload=True) 36 | raw.pick_types(ecog=True) 37 | raw = helper.reject_channels(raw, participant, experiment) 38 | 39 | for i_peak, peak in enumerate(peaks): 40 | 41 | file_name = "%s/ssd_%s_peak_%.2f.npy" % ( 42 | results_dir, 43 | participant_id, 44 | peak, 45 | ) 46 | bin_width = bin_width1[i_peak] 47 | 48 | filters, patterns = ssd.run_ssd(raw, peak, bin_width) 49 | 50 | data_dict = dict(peak=peak, filters=filters, patterns=patterns) 51 | np.save(file_name, data_dict) 52 | -------------------------------------------------------------------------------- /csv/retained_components.csv: -------------------------------------------------------------------------------- 1 | participant,experiment,frequency,bandwidth 2 | hh,fixation_pwrlaw,5.0,1.75 3 | hh,fixation_pwrlaw,7.5,2.5 4 | hh,fixation_pwrlaw,15.2,2.5 5 | hh,fixation_pwrlaw,10.5,2 6 | hh,fixation_pwrlaw,12.6,1.5 7 | rr,fixation_pwrlaw,8.9,2 8 | rr,fixation_pwrlaw,12.7,1.5 9 | rr,fixation_pwrlaw,14.5,1.5 10 | rr,fixation_pwrlaw,17.5,2.5 11 | rr,fixation_pwrlaw,20.0,3.5 12 | gc,fixation_pwrlaw,14.9,3 13 | gc,fixation_pwrlaw,18.8,2.5 14 | gc,fixation_pwrlaw,7.5,2 15 | gc,fixation_pwrlaw,12.3,1.5 16 | gw,fixation_pwrlaw,14.0,3. 17 | jp,fixation_pwrlaw,5.5,1.5 18 | jp,fixation_pwrlaw,12.6,2.5 19 | jc,fixation_pwrlaw,9.1,2 20 | jc,fixation_pwrlaw,6.1,2.5 21 | jc,fixation_pwrlaw,12.9,1.5 22 | fp,fixation_pwrlaw,7.3,2.5 23 | fp,fixation_pwrlaw,5.5,2.5 24 | cc,fixation_pwrlaw,15,3 25 | wm,fixation_pwrlaw,7,2 26 | wm,fixation_pwrlaw,14,2 27 | wm,fixation_pwrlaw,21.4,6 28 | gf,fixation_pwrlaw,7.2,1.5 29 | gf,fixation_pwrlaw,5.1,2.5 30 | de,fixation_pwrlaw,7.3,2.5 31 | zt,fixation_pwrlaw,8.0,1.5 32 | zt,fixation_pwrlaw,15.5,1.5 33 | zt,fixation_pwrlaw,5.1,1.5 34 | ca,fixation_pwrlaw,11.0,1.5 35 | ca,fixation_pwrlaw,7.5,1.5 36 | ca,fixation_pwrlaw,15.1,1.5 37 | ca,fixation_pwrlaw,18.6,1.5 38 | al,fixation_pwrlaw,14.1,3.5 39 | al,fixation_pwrlaw,5.6,2.5 40 | h0,fixation_pwrlaw,8.1,2.5 41 | h0,fixation_pwrlaw,12.6,2.5 42 | h0,fixation_pwrlaw,20.5,1.5 43 | h0,fixation_pwrlaw,24.1,1.5 44 | wc,fixation_pwrlaw,8.5,1.5 45 | wc,fixation_pwrlaw,6.1,1.5 46 | wc,fixation_pwrlaw,12.3,1.5 47 | jm,fixation_pwrlaw,5.4,2 48 | jm,fixation_pwrlaw,8.2,1.5 49 | mv,fixation_pwrlaw,7.3,2 50 | mv,fixation_pwrlaw,13.5,2.5 51 | mv,fixation_pwrlaw,24.8,4 52 | mv,fixation_pwrlaw,16.9,1.5 53 | ug,fixation_pwrlaw,9.1,2.5 54 | ug,fixation_pwrlaw,7.0,1.5 55 | rh,fixation_pwrlaw,8.3,1.5 56 | rh,fixation_pwrlaw,16.4,3.5 57 | rh,fixation_pwrlaw,18.5,3.5 58 | rh,fixation_pwrlaw,4.7,1.5 59 | rh,fixation_pwrlaw,13.8,1.5 60 | -------------------------------------------------------------------------------- /csv/selected_datasets.csv: -------------------------------------------------------------------------------- 1 | participant,file_name,experiment,elec_file,electrodes,is_rest 2 | hh,../data/fixation_pwrlaw/data/hh_base.mat,fixation_pwrlaw,,"[]",True 3 | rr,../data/fixation_pwrlaw/data/rr_base.mat,fixation_pwrlaw,,"[8,9,43,44,58,37,33,31,30,29,28,15]",True 4 | gc,../data/fixation_pwrlaw/data/gc_base.mat,fixation_pwrlaw,,"[]",True 5 | gw,../data/fixation_pwrlaw/data/gw_base.mat,fixation_pwrlaw,,"[]",True 6 | jp,../data/fixation_pwrlaw/data/jp_base.mat,fixation_pwrlaw,,"[48,37,35,53]",True 7 | jc,../data/fixation_pwrlaw/data/jc_base.mat,fixation_pwrlaw,,"[]",True 8 | fp,../data/fixation_pwrlaw/data/fp_base.mat,fixation_pwrlaw,,"[2,4,63]",True 9 | cc,../data/fixation_pwrlaw/data/cc_base.mat,fixation_pwrlaw,,"[]",True 10 | wm,../data/fixation_pwrlaw/data/wm_base.mat,fixation_pwrlaw,,"[33]",True 11 | gf,../data/fixation_pwrlaw/data/gf_base.mat,fixation_pwrlaw,,"[57]",True 12 | de,../data/fixation_pwrlaw/data/de_base.mat,fixation_pwrlaw,,"[]",True 13 | zt,../data/fixation_pwrlaw/data/zt_base.mat,fixation_pwrlaw,,"[57,58]",True 14 | ca,../data/fixation_pwrlaw/data/ca_base.mat,fixation_pwrlaw,,"[]",True 15 | al,../data/fixation_pwrlaw/data/al_base.mat,fixation_pwrlaw,,"[]",True 16 | h0,../data/fixation_pwrlaw/data/h0_base.mat,fixation_pwrlaw,,"[]",True 17 | wc,../data/fixation_pwrlaw/data/wc_base.mat,fixation_pwrlaw,,"[]",True 18 | jm,../data/fixation_pwrlaw/data/jm_base.mat,fixation_pwrlaw,,"[]",True 19 | mv,../data/fixation_pwrlaw/data/mv_base.mat,fixation_pwrlaw,,"[59]",True 20 | ug,../data/fixation_pwrlaw/data/ug_base.mat,fixation_pwrlaw,,"[]",True 21 | rh,../data/fixation_pwrlaw/data/rh_base.mat,fixation_pwrlaw,,"[]",True 22 | ug,../data/motor_basic/data/ug_mot_t_h.mat,motor_basic,../data/motor_basic/locs/ug_electrodes.mat,"[]",False 23 | ja,../data/faces_basic/data/ja/ja_faceshouses.mat,faces_basic,../data/faces_basic/locs/ja_xslocs.mat,"[]",False 24 | jt,../data/faces_basic/data/jt/jt_faceshouses.mat,faces_basic,../data/faces_basic/locs/jt_xslocs.mat,"[64,66,91,96,67,68,69,65,101,74,77,76,75,70,71]",False 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Enhancing oscillations in intracranial electrophysiological recordings with data-driven spatial filters 2 | 3 | This repository provides analysis code to compute data-driven spatial filters using spatio-spectral decomposition in intracranial electrophysiological data. The repository code recreates results and figures from the following manuscript: 4 | 5 | # Reference 6 | Schaworonkow N & Voytek B: [Enhancing oscillations in intracranial electrophysiological recordings with data-driven spatial filters](https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1009298) _PLoS Computional Biology_ (2021). [doi:10.1371/journal.pcbi.1009298](https://doi.org/10.1371/journal.pcbi.1009298). 7 | 8 | 9 | # Dataset 10 | The results are based on following available openly available data set: [A library of human electrocorticographic data and analyses.](https://exhibits.stanford.edu/data/catalog/zk881ps0522) which is described in detail in following article: 11 | 12 | Miller, K.J. A library of human electrocorticographic data and analyses. _Nature Human Behavior_ 3, 1225–1235 (2019). [doi:10.1038/s41562-019-0678-3](https://doi.org/10.1038/s41562-019-0678-3). 13 | 14 | To reproduce the figures from the spatial filters manuscript, the data set should be downloaded and placed in the folder ```data```. 15 | 16 | # Requirements 17 | 18 | The provided python3 scripts are using ```scipy``` and ```numpy``` for general computation, ```pandas``` for saving intermediate results to csv-files. ```matplotlib``` for visualization. For EEG-related analysis, the ```mne``` package is used. For computation of aperiodic exponents: [```fooof```](https://fooof-tools.github.io/fooof/) and for computation of waveform features: [```bycycle```](https://bycycle-tools.github.io/bycycle/). Specifically used versions can be seen in the ```requirements.txt```. 19 | 20 | 21 | # Pipeline 22 | 23 | To reproduce the figures from the command line, navigate into the ```code``` folder and execute ```make all```. This will run through the preprocessing steps, the computation of spatial filters, the analysis of peak frequencies and the oscillatory burst analysis. The scripts can also be executed separately in the order described in the ```Makefile```. 24 | -------------------------------------------------------------------------------- /code/proc_3_calculate_bursts_ssd.py: -------------------------------------------------------------------------------- 1 | """ This script performs burst detection across all resting datasets. 2 | The same burst detection parameters are used across datasets. The output 3 | is saved in the form of csv-files. 4 | """ 5 | 6 | import mne 7 | import helper 8 | import ssd 9 | import pandas as pd 10 | import os 11 | from bycycle.features import compute_features 12 | 13 | df = pd.read_csv("../csv/selected_datasets.csv", index_col=False) 14 | df = df[df.is_rest] 15 | 16 | participants = df.participant 17 | experiments = df.experiment 18 | 19 | results_folder = "../results/bursts/" 20 | os.makedirs(results_folder, exist_ok=True) 21 | 22 | features = [ 23 | "period", 24 | "time_trough", 25 | "time_peak", 26 | "volt_trough", 27 | "volt_peak", 28 | "time_rise", 29 | "time_decay", 30 | "volt_rise", 31 | "volt_decay", 32 | "volt_amp", 33 | "time_rdsym", 34 | "time_ptsym", 35 | ] 36 | 37 | # -- setting the same burst detection parameters for all datasets 38 | osc_param = { 39 | "amplitude_fraction_threshold": 0.75, 40 | "amplitude_consistency_threshold": 0.5, 41 | "period_consistency_threshold": 0.5, 42 | "monotonicity_threshold": 0.5, 43 | "N_cycles_min": 3, 44 | } 45 | 46 | freq_width = 3 47 | 48 | for i_sub, (participant, exp) in enumerate(zip(participants, experiments)): 49 | print(participant, exp) 50 | dfs = [] 51 | 52 | participant_id = "%s_%s" % (participant, exp) 53 | raw_file = "../working/%s_raw.fif" % participant_id 54 | raw = mne.io.read_raw_fif(raw_file) 55 | raw.load_data() 56 | raw.pick_types(ecog=True) 57 | raw = helper.reject_channels(raw, participant, exp) 58 | 59 | # get individual peaks 60 | peaks = helper.get_participant_peaks(participant, exp) 61 | 62 | for peak1 in peaks: 63 | 64 | patterns, filters = helper.load_ssd(participant_id, peak1) 65 | raw_ssd = ssd.apply_filters(raw, filters[:, :2]) 66 | bandwidth = (float(peak1) - freq_width, float(peak1) + freq_width) 67 | i_comp = 0 68 | 69 | # compute features for SSD component 70 | df = compute_features( 71 | raw_ssd._data[i_comp], 72 | raw.info["sfreq"], 73 | f_range=bandwidth, 74 | burst_detection_kwargs=osc_param, 75 | center_extrema="T", 76 | ) 77 | 78 | # save mean burst features 79 | df = df[df.is_burst] 80 | nr_bursts = len(df) 81 | df1 = df.mean() 82 | df1 = df1[features] 83 | df1["comp"] = i_comp 84 | df1["nr_bursts"] = nr_bursts 85 | 86 | df_file_name = "%s/bursts_%s_peak_%s.csv" % ( 87 | results_folder, 88 | participant_id, 89 | peak1, 90 | ) 91 | 92 | df1 = df1.reset_index() 93 | df1.to_csv(df_file_name, header=["feature", "value"], index=False) 94 | -------------------------------------------------------------------------------- /code/proc_1_calculate_spectral_param_electrodes.py: -------------------------------------------------------------------------------- 1 | """ This script calculates a spectral parametrization of power spectra. 2 | The calculation is performed for all electrodes, to inform selection of 3 | peak frequencies for calculation of data-driven spatial filters. 4 | """ 5 | 6 | import mne 7 | import numpy as np 8 | import matplotlib.pyplot as plt 9 | import helper 10 | import pandas as pd 11 | import fooof 12 | import os 13 | 14 | df_sub = pd.read_csv("../csv/selected_datasets.csv", index_col=False) 15 | df_sub = df_sub[df_sub.is_rest] 16 | 17 | participants = df_sub.participant 18 | experiments = df_sub.experiment 19 | df_sub = df_sub.set_index("participant") 20 | nr_seconds = 3 21 | fmax = 55 22 | fmin = 2 23 | 24 | 25 | for i_sub, (participant, exp) in enumerate(zip(participants, experiments)): 26 | print(participant, exp) 27 | dfs = [] 28 | 29 | participant_id = "%s_%s" % (participant, exp) 30 | raw_file = "../working/%s_raw.fif" % participant_id 31 | raw = mne.io.read_raw_fif(raw_file) 32 | raw.load_data() 33 | raw.pick_types(ecog=True) 34 | raw = helper.reject_channels(raw, participant, exp) 35 | 36 | psd, freq = mne.time_frequency.psd_welch( 37 | raw, 38 | fmin=fmin, 39 | fmax=fmax, 40 | n_fft=nr_seconds * int(raw.info["sfreq"]), 41 | ) 42 | 43 | # calculate spectral parametrization for each channel 44 | plot_folder = "../results/psd_param/%s/" % participant 45 | os.makedirs(plot_folder, exist_ok=True) 46 | 47 | # always plot 9 electrodes together in a figure for saving some space 48 | fig, ax = plt.subplots(3, 3) 49 | counter = 0 50 | i_fig = 0 51 | for i in range(len(raw.ch_names)): 52 | ax1 = ax.flatten()[counter] 53 | fg = fooof.FOOOF(max_n_peaks=5, min_peak_height=0.5) 54 | fg.fit(freq, psd[i]) 55 | 56 | peak_params = fg.get_params("peak_params") 57 | 58 | if not np.isnan(peak_params.flatten()[0]): 59 | fg.plot( 60 | ax=ax1, 61 | add_legend=False, 62 | plot_style=None, 63 | plot_peaks="line", 64 | ) 65 | else: 66 | fg.plot( 67 | ax=ax1, 68 | add_legend=False, 69 | plot_style=None 70 | ) 71 | 72 | # if peaks were identified print them into axes 73 | if not np.isnan(peak_params.flatten()[0]): 74 | ax1.text( 75 | 15, 76 | ax1.get_ylim()[1] - 0.05 * ax1.get_ylim()[1], 77 | peak_params, 78 | va="top", 79 | color="tab:green", 80 | fontsize=10, 81 | ) 82 | ax1.set_title(raw.ch_names[i]) 83 | ax1.set(xlim=(fmin, fmax), xlabel="frequency [Hz]", yticklabels=[]) 84 | counter += 1 85 | if counter == 9: 86 | fig.set_size_inches(8, 8) 87 | fig.tight_layout() 88 | fig.savefig("%s/spec_param_%i.png" % (plot_folder, i_fig)) 89 | fig.show() 90 | plt.close("all") 91 | fig, ax = plt.subplots(3, 3) 92 | counter = 0 93 | i_fig += 1 94 | -------------------------------------------------------------------------------- /code/fig_5a_variability_plot_on_3d_surface.py: -------------------------------------------------------------------------------- 1 | """ Creates 3d brain + all components with SNR > threshold plots for all 2 | participants. 3 | """ 4 | import mne 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | import helper 8 | import pandas as pd 9 | import pyvista as pv 10 | import os 11 | 12 | df = pd.read_csv("../csv/selected_datasets.csv", index_col=False) 13 | df = df[df.is_rest] 14 | 15 | participants = df.participant 16 | experiments = df.experiment 17 | 18 | nr_components = 10 19 | nr_seconds = 5 20 | results_folder = "../results/spec_param/" 21 | plot_folder = "../figures/3dbrains/" 22 | 23 | os.makedirs(results_folder, exist_ok=True) 24 | os.makedirs(plot_folder, exist_ok=True) 25 | 26 | # -- for creating peak frequency colorbar 27 | peaks_n = np.arange(5, 21, 1) 28 | N = len(peaks_n) 29 | cmap = [plt.cm.viridis(i) for i in np.linspace(0.2, 1, N)] 30 | 31 | pv.set_plot_theme("document") 32 | cpos = helper.get_camera_position() 33 | brain_cloud = helper.plot_3d_brain() 34 | 35 | snr_threshold = 0.5 36 | max_nr_components = 10 37 | 38 | for i_sub, (participant, exp) in enumerate(zip(participants, experiments)): 39 | dfs = [] 40 | 41 | participant_id = "%s_%s" % (participant, exp) 42 | 43 | plot_filename = "%s/localization_%s_spatial_max.png" % ( 44 | plot_folder, 45 | participant_id, 46 | ) 47 | 48 | raw_file = "../working/%s_raw.fif" % participant_id 49 | raw = mne.io.read_raw_fif(raw_file) 50 | raw.crop(0, 1) 51 | raw.load_data() 52 | raw.pick_types(ecog=True) 53 | 54 | electrodes = helper.plot_electrodes(raw) 55 | 56 | # create mesh plot 57 | plotter = pv.Plotter(off_screen=True) 58 | actor = plotter.add_mesh(brain_cloud, color="w") 59 | act_electrodes = plotter.add_mesh(electrodes, point_size=25, color="w") 60 | 61 | # get individual peaks 62 | peaks = helper.get_participant_peaks(participant, exp) 63 | 64 | for peak in peaks: 65 | 66 | # load spectral parametrization of SSD traces 67 | df_file = "%s/sources_%s_peak_%.2f.csv" % ( 68 | results_folder, 69 | participant_id, 70 | peak, 71 | ) 72 | df = pd.read_csv(df_file) 73 | df = df[df.snr > snr_threshold] 74 | 75 | nr_components = np.min([max_nr_components, len(df)]) 76 | 77 | for i_comp in range(nr_components): 78 | location = df.iloc[i_comp][["x", "y", "z"]].to_numpy() 79 | max_peak = df.iloc[i_comp]["freq"] 80 | snr = df.iloc[i_comp]["snr"] 81 | 82 | # make circle with color according to frequency 83 | cyl = pv.PolyData(location) 84 | idx_color = np.argmin(np.abs(peaks_n - max_peak)) 85 | color = cmap[idx_color] 86 | 87 | actor = plotter.add_mesh( 88 | cyl, 89 | point_size=60 * snr, 90 | color=color, 91 | render_points_as_spheres=True, 92 | ) 93 | 94 | plotter.show( 95 | cpos=cpos, 96 | title=str(cpos), 97 | interactive_update=False, 98 | screenshot=plot_filename, 99 | ) 100 | plotter.screenshot(plot_filename, transparent_background=True) 101 | plotter.close() 102 | -------------------------------------------------------------------------------- /code/fig_6a_plot_waveshape_on_3dbrain.py: -------------------------------------------------------------------------------- 1 | """ Plots the asymmetry index on a 3d brain. 2 | """ 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | import helper 6 | import pandas as pd 7 | import os 8 | import pyvista as pv 9 | 10 | df = pd.read_csv("../csv/selected_datasets.csv", index_col=False) 11 | df = df[df.is_rest] 12 | 13 | participants = df.participant 14 | experiments = df.experiment 15 | pv.set_plot_theme("document") 16 | nr_components = 10 17 | nr_seconds = 5 18 | 19 | results_folder = "../results/bursts/" 20 | results_folder2 = "../results/spec_param/" 21 | plot_folder = "../results/3dbrains/" 22 | 23 | os.makedirs(results_folder, exist_ok=True) 24 | os.makedirs(plot_folder, exist_ok=True) 25 | 26 | peaks_n = np.arange(5, 21, 1) 27 | peaks_n = np.arange(0, 0.21, 0.01) 28 | N = len(peaks_n) 29 | cmap = [plt.cm.plasma(i) for i in np.linspace(0.2, 1, N)] 30 | 31 | cpos = helper.get_camera_position() 32 | cloud = helper.plot_3d_brain() 33 | 34 | 35 | # -- create 3d brain plot 36 | plotter = pv.Plotter(off_screen=True) 37 | actor = plotter.add_mesh(cloud) 38 | 39 | for i_sub, (participant, exp) in enumerate(zip(participants, experiments)): 40 | 41 | participant_id = "%s_%s" % (participant, exp) 42 | 43 | plot_filename = "%s/asymmetry_%s_spatial_max.png" % ( 44 | plot_folder, 45 | participant_id, 46 | ) 47 | 48 | # get individual peaks 49 | peaks = helper.get_participant_peaks(participant, exp) 50 | 51 | for peak1 in peaks: 52 | 53 | df_file_name = "%s/bursts_%s_peak_%s.csv" % ( 54 | results_folder, 55 | participant_id, 56 | peak1, 57 | ) 58 | 59 | df = pd.read_csv(df_file_name) 60 | df = df.set_index("feature") 61 | df = df.T 62 | frequency = 1000 / df.period 63 | if np.any(frequency < 5): 64 | continue 65 | 66 | asymmetry = 2 * np.abs(df["time_ptsym"] - 0.5) 67 | asymmetry = asymmetry.to_numpy() 68 | 69 | peak1 = float(peak1) 70 | df_file = "%s/sources_%s_peak_%.2f.csv" % ( 71 | results_folder2, 72 | participant_id, 73 | peak1, 74 | ) 75 | df = pd.read_csv(df_file) 76 | df = df[df.snr > 0.5] 77 | 78 | nr_components = np.min([1, len(df)]) 79 | 80 | for i_comp in range(nr_components): 81 | location = df.iloc[i_comp][["x", "y", "z"]].to_numpy() 82 | location[0] += 2 83 | max_peak = df.iloc[i_comp]["freq"] 84 | snr = df.iloc[i_comp]["snr"] 85 | 86 | # make circle with color according to frequency 87 | cyl = pv.PolyData(location) 88 | idx_color = np.argmin(np.abs(peaks_n - asymmetry)) 89 | 90 | color = cmap[idx_color] 91 | 92 | actor = plotter.add_mesh( 93 | cyl, 94 | point_size=snr * 30, 95 | color=color, 96 | render_points_as_spheres=True, 97 | ) 98 | 99 | plot_filename = "../figures/3dbrain_asymmetry.png" 100 | 101 | plotter.show( 102 | cpos=cpos, 103 | title=str(cpos), 104 | interactive_update=False, 105 | screenshot=plot_filename, 106 | ) 107 | 108 | plotter.screenshot(plot_filename, transparent_background=True) 109 | plotter.close() 110 | -------------------------------------------------------------------------------- /code/proc_4_calculate_spectral_param_ssd.py: -------------------------------------------------------------------------------- 1 | """ This script calculates a spectral parametrization of SSD-filtered spectra. 2 | The calculation is performed for all participants, the output in terms of 3 | extracted oscillatory peak parameters (peak frequency and SNR) is saved 4 | in the form of csv-files. 5 | """ 6 | import mne 7 | import numpy as np 8 | import helper 9 | import ssd 10 | import pandas as pd 11 | import fooof 12 | import os 13 | 14 | df = pd.read_csv("../csv/selected_datasets.csv", index_col=False) 15 | df = df[df.is_rest] 16 | participants = df.participant 17 | experiments = df.experiment 18 | 19 | nr_seconds = 3 20 | fmax = 55 21 | fmin = 2 22 | 23 | results_folder = "../results/spec_param/" 24 | os.makedirs(results_folder, exist_ok=True) 25 | 26 | for i_sub, (participant, exp) in enumerate(zip(participants, experiments)): 27 | print(participant, exp) 28 | 29 | participant_id = "%s_%s" % (participant, exp) 30 | raw_file = "../working/%s_raw.fif" % participant_id 31 | raw = mne.io.read_raw_fif(raw_file) 32 | raw.load_data() 33 | raw.pick_types(ecog=True) 34 | raw = helper.reject_channels(raw, participant, exp) 35 | 36 | # get individual peaks 37 | peaks = helper.get_participant_peaks(participant, exp) 38 | n_fft = nr_seconds * int(raw.info["sfreq"]) 39 | for peak in peaks: 40 | 41 | print(participant_id, peak) 42 | 43 | df_file_name = "%s/sources_%s_peak_%.2f.csv" % ( 44 | results_folder, 45 | participant_id, 46 | peak, 47 | ) 48 | 49 | patterns, filters = helper.load_ssd(participant_id, peak) 50 | nr_components = np.min([10, patterns.shape[1]]) 51 | 52 | raw_ssd = ssd.apply_filters(raw, filters[:, :nr_components]) 53 | psd, freq = mne.time_frequency.psd_welch( 54 | raw_ssd, fmin=fmin, fmax=fmax, n_fft=n_fft 55 | ) 56 | 57 | fg = fooof.FOOOFGroup(max_n_peaks=5) 58 | fg.fit(freq, psd) 59 | 60 | peak_params = fg.get_params("peak_params") 61 | max_peaks = fooof.analysis.periodic.get_band_peak_fg(fg, [fmin, fmax]) 62 | 63 | # for each component find electrode with maximum spatial pattern coef 64 | locations = helper.compute_spatial_max( 65 | patterns[:, :nr_components], raw, plot=False 66 | ) 67 | 68 | dfs = [] 69 | for i_comp in range(nr_components): 70 | abs_pattern = np.abs(patterns[:, i_comp]) 71 | idx_chan = np.argmax(abs_pattern) 72 | 73 | max_peak = max_peaks[i_comp][0] 74 | snr = max_peaks[i_comp, 1] 75 | 76 | data = ( 77 | locations[i_comp, 0], 78 | locations[i_comp, 1], 79 | locations[i_comp, 2], 80 | max_peak, 81 | snr, 82 | i_comp, 83 | ) 84 | 85 | df = pd.Series( 86 | data, 87 | index=[ 88 | "x", 89 | "y", 90 | "z", 91 | "freq", 92 | "snr", 93 | "i_comp", 94 | ], 95 | ) 96 | 97 | dfs.append(df) 98 | df = pd.concat(dfs, axis=1).T 99 | df.to_csv(df_file_name) 100 | -------------------------------------------------------------------------------- /code/proc_0_readin_to_mne.py: -------------------------------------------------------------------------------- 1 | """ This script unifies sampling frequency and channel names across datasets 2 | and saves all provided mat-files into mne fif-file format. 3 | """ 4 | import mne 5 | import numpy as np 6 | import scipy.io 7 | import pandas as pd 8 | import os 9 | 10 | df = pd.read_csv("../csv/selected_datasets.csv") 11 | raw_folder = "../working/" 12 | os.makedirs(raw_folder, exist_ok=True) 13 | 14 | for i in range(len(df)): 15 | 16 | participant = df.iloc[i].participant 17 | experiment = df.iloc[i].experiment 18 | data_file = df.iloc[i].file_name 19 | print(experiment, participant) 20 | 21 | raw_file = "%s/%s_%s_raw.fif" % (raw_folder, participant, experiment) 22 | data = scipy.io.loadmat(data_file) 23 | 24 | # extract electrode locations which are either saved together with the data 25 | # or in a separate file 26 | if "locs" in data.keys(): 27 | electrodes = data["locs"] 28 | elif "electrodes" in data.keys(): 29 | electrodes = data["electrodes"] 30 | elif experiment != "fixation_pwrlaw": 31 | electrode_file = df.iloc[i].elec_file 32 | locs = scipy.io.loadmat(electrode_file) 33 | if "locs" in locs.keys(): 34 | electrodes = locs["locs"] 35 | elif "electrodes" in locs.keys(): 36 | electrodes = locs["electrodes"] 37 | 38 | # set sampling frequency manually, except where there is a specific entry 39 | sfreq = 1000 40 | if participant == "gw": 41 | sfreq = 10000 42 | if "srate" in data.keys(): 43 | sfreq = int(data["srate"][0][0]) 44 | 45 | ecog = data["data"].T 46 | nr_ecog_channels = ecog.shape[0] 47 | ch_names_ecog = ["ecog%i" % i for i in range(nr_ecog_channels)] 48 | 49 | # create stim channel in case there are annotations for events 50 | if "stim" in data.keys(): 51 | stim = data["stim"].T 52 | 53 | # combine all different types of channels 54 | data = np.vstack((ecog, stim)) 55 | ch_types = np.hstack((np.tile("ecog", nr_ecog_channels), "stim")) 56 | channels = ch_names_ecog + ["stim"] 57 | else: 58 | data = ecog 59 | ch_types = np.tile("ecog", nr_ecog_channels) 60 | channels = ch_names_ecog 61 | 62 | # create montage 63 | dig_ch_pos = dict(zip(ch_names_ecog, electrodes)) 64 | montage = mne.channels.make_dig_montage( 65 | ch_pos=dig_ch_pos, coord_frame="head" 66 | ) 67 | 68 | # create raw object 69 | info = mne.create_info(channels, sfreq, ch_types=ch_types) 70 | raw = mne.io.RawArray(data, info) 71 | raw.set_montage(montage) 72 | 73 | # if the data has event-related markers, save them as annotations 74 | if "stim" in channels: 75 | events = mne.find_events(raw) 76 | 77 | if experiment == "motor_basic": 78 | mapping = dict(tongue=11, hand=12) 79 | if experiment == "faces_basic": 80 | mapping = dict(isi=101) 81 | 82 | mapping = {v: k for k, v in mapping.items()} 83 | 84 | onsets = events[:, 0] / raw.info["sfreq"] 85 | durations = np.zeros_like(onsets) # assumes instantaneous events 86 | descriptions = [mapping[event_id] for event_id in events[:, 2]] 87 | annot_from_events = mne.Annotations( 88 | onset=onsets, 89 | duration=durations, 90 | description=descriptions, 91 | orig_time=raw.info["meas_date"], 92 | ) 93 | raw.set_annotations(annot_from_events) 94 | 95 | if raw.info["sfreq"] > 1000: 96 | raw.resample(1000) 97 | 98 | # save raw 99 | raw.save(raw_file, overwrite=True) 100 | -------------------------------------------------------------------------------- /code/fig_7_noise_cleaning.py: -------------------------------------------------------------------------------- 1 | """ Demonstrates noise reduction with spatial filters. 2 | """ 3 | import mne 4 | import numpy as np 5 | import helper 6 | import ssd 7 | import matplotlib.pyplot as plt 8 | import matplotlib.gridspec as gridspec 9 | 10 | 11 | def plot_timeseries(raw, ax1, picks, tmin, nr_seconds=2): 12 | tmax = tmin + nr_seconds 13 | raw_picks = raw.copy().pick(picks).crop(tmin, tmax) 14 | for i in range(len(picks)): 15 | signal = raw_picks._data[i] 16 | signal = signal / np.ptp(signal) 17 | ax1.plot(raw_picks.times, signal + i, color="k", lw=0.5) 18 | ax1.axis("off") 19 | 20 | 21 | def plot_psd(raw, ax1): 22 | raw.plot_psd(fmin=2, fmax=220, ax=ax1) 23 | ax1.set_title("") 24 | ax1.set_ylabel("") 25 | ax1.grid(False) 26 | ax1.set(xticks=[1, 60, 100, 200]) 27 | ax1.set_xlabel("frequency [Hz]") 28 | ax1.set_yticklabels([]) 29 | 30 | 31 | # -- load continuous data 32 | participant = "jt" 33 | experiment = "faces_basic" 34 | data_file = "../working/%s_%s_raw.fif" % (participant, experiment) 35 | raw = mne.io.read_raw_fif(data_file) 36 | raw.load_data() 37 | raw.pick_types(ecog=True) 38 | raw = helper.reject_channels(raw, participant, experiment) 39 | 40 | # -- create common average referenced traces 41 | raw_car = raw.copy() 42 | raw_car.set_eeg_reference("average") 43 | 44 | # -- select some channels and and example time point 45 | picks = [86, 17, 19, 21, 1, 50] 46 | tmin = 88 47 | 48 | plt.ion() 49 | 50 | fig = plt.figure() 51 | gs = gridspec.GridSpec(2, 3) 52 | 53 | 54 | # -- plot raw traces and spectrum 55 | ax1 = fig.add_subplot(gs[0, 0]) 56 | plot_timeseries(raw, ax1, picks, tmin=tmin) 57 | ax1.set_title("raw time series") 58 | 59 | ax0 = plt.subplot(gs[1, 0]) 60 | plot_psd(raw, ax0) 61 | 62 | # -- plot common average referenced traces and spectrum 63 | ax1 = plt.subplot(gs[0, 2]) 64 | plot_timeseries(raw_car, ax1, picks, tmin=tmin) 65 | ax1.set_title("common-average\nreferencing") 66 | 67 | ax1 = fig.add_subplot(gs[1, 2], sharey=ax0) 68 | plot_psd(raw_car, ax1) 69 | 70 | 71 | # -- remove noise with SSD, first for 200 Hz 72 | print("estimate noise components...") 73 | peak = 200 74 | bin_width = 1.75 75 | signal_bp = [peak - bin_width, peak + bin_width] 76 | noise_bp = [1, peak + (bin_width + 2)] 77 | noise_bs = [peak - (bin_width + 1), peak + (bin_width + 1)] 78 | filters, patterns = ssd.compute_ssd(raw, signal_bp, noise_bp, noise_bs) 79 | 80 | # -- determine number of patterns to remove for 200 Hz 81 | nr_patterns = 3 82 | raw_ssd = ssd.apply_filters(raw, filters[:, :nr_patterns]) 83 | noise = patterns[:, :nr_patterns] @ raw_ssd._data[:nr_patterns] 84 | raw._data = raw._data - noise 85 | 86 | # -- remove noise with SSD, then for 60 Hz 87 | peak = 60 88 | bin_width = 1.75 89 | signal_bp = [peak - bin_width, peak + bin_width] 90 | noise_bp = [1, peak + (bin_width + 2)] 91 | noise_bs = [peak - (bin_width + 1), peak + (bin_width + 1)] 92 | filters, patterns = ssd.compute_ssd(raw, signal_bp, noise_bp, noise_bs) 93 | 94 | # -- determine number of patterns to remove 95 | nr_patterns = 2 96 | raw_ssd = ssd.apply_filters(raw, filters[:, :nr_patterns]) 97 | noise = patterns[:, :nr_patterns] @ raw_ssd._data[:nr_patterns] 98 | raw._data = raw._data - noise 99 | 100 | 101 | # -- plot PSD and time series after SSD removal 102 | ax2 = fig.add_subplot(gs[1, 1], sharey=ax0) 103 | plot_psd(raw, ax2) 104 | 105 | ax1 = plt.subplot(gs[0, 1]) 106 | plot_timeseries(raw, ax1, picks, tmin=tmin) 107 | ax1.set_title("noise removal with SSD\n for 60 and 200 Hz") 108 | 109 | # -- remove tiny topos 110 | topos = [t for t in fig.get_children() if "mpl" in str(type(t))] 111 | [t.remove() for t in topos] 112 | 113 | fig.set_size_inches(7.5, 5) 114 | fig.savefig("../figures/fig7_line_noise.pdf", dpi=300) 115 | fig.show() 116 | -------------------------------------------------------------------------------- /code/fig_5b_variability_combine_in_plot.py: -------------------------------------------------------------------------------- 1 | """ Combines participant 3d brain plots into figure. 2 | """ 3 | import mne 4 | import numpy as np 5 | import helper 6 | import matplotlib.pyplot as plt 7 | import matplotlib.gridspec as gridspec 8 | import pandas as pd 9 | import matplotlib.image as mpimg 10 | import matplotlib as mpl 11 | 12 | 13 | df2 = pd.read_csv("../csv/selected_datasets.csv") 14 | df2 = df2[df2.is_rest] 15 | 16 | participants = df2.participant 17 | experiments = df2.experiment 18 | results_dir = "../results/spec_param/" 19 | folder_brains = "../figures/3dbrains/" 20 | 21 | # -- set parameters 22 | snr_threshold = 0.5 23 | min_freq = 5 24 | max_freq = 20 25 | max_nr_components = 10 26 | 27 | # -- plot colorbar; to get this right a dummy figure must be created 28 | peaks_n = np.arange(5, 21, 1) 29 | N = len(peaks_n) 30 | cmap = [plt.cm.viridis(i) for i in np.linspace(0.2, 1, N)] 31 | 32 | fig2 = plt.figure() 33 | for i in range(N): 34 | sc = plt.scatter(i, peaks_n[i], s=10, color=cmap[i]) 35 | plt.close(fig2) 36 | 37 | # -- create figure 38 | fig = plt.figure() 39 | 40 | gs1 = gridspec.GridSpec( 41 | 2, 42 | 2, 43 | width_ratios=[40, 1], 44 | height_ratios=[2, 1], 45 | ) 46 | 47 | 48 | ax1 = plt.subplot(gs1[0, 1]) 49 | cb = plt.colorbar(sc, cax=ax1) 50 | cb.set_ticks(np.linspace(0, 1, N)) 51 | cb.set_ticklabels(peaks_n) 52 | cb.set_label("peak frequency [Hz]") 53 | 54 | 55 | gs = gridspec.GridSpecFromSubplotSpec( 56 | 5, 57 | 4, 58 | subplot_spec=gs1[0, 0], 59 | ) 60 | 61 | 62 | # -- determine order of participants according to electrode mean y-position 63 | mean_loc = np.zeros((len(participants), 3)) 64 | for i_sub, (participant, exp) in enumerate(zip(participants, experiments)): 65 | participant_id = "%s_%s" % (participant, exp) 66 | file_name = "../working/%s_raw.fif" % participant_id 67 | raw = mne.io.read_raw_fif(file_name) 68 | 69 | electrodes = np.array([e["loc"][:3] for e in raw.info["chs"]]) 70 | mean_loc[i_sub] = electrodes.mean(axis=0) 71 | 72 | 73 | idx = np.argsort(mean_loc[:, 2])[::-1] 74 | participants = participants[idx] 75 | experiments = experiments[idx] 76 | 77 | # -- plot small 3d brains 78 | for i_s, (participant, exp) in enumerate(zip(participants, experiments)): 79 | file_name = "%s/localization_%s_%s_spatial_max.png" % ( 80 | folder_brains, 81 | participant, 82 | exp, 83 | ) 84 | 85 | ax1 = plt.subplot(gs[i_s]) 86 | img = mpimg.imread(file_name) 87 | ax1.imshow(img) 88 | ax1.set(yticks=[], xticks=[]) 89 | ax1.axis("off") 90 | 91 | 92 | # -- plot peak frequencies across space for each participant 93 | 94 | norm = mpl.colors.Normalize(vmin=-65.0, vmax=65.0) 95 | N = len(df2) 96 | colors = [plt.cm.cividis(i) for i in np.linspace(0, 1, N)] 97 | 98 | gs = gridspec.GridSpecFromSubplotSpec( 99 | 1, 100 | 3, 101 | subplot_spec=gs1[1, :], 102 | width_ratios=[3, 1, 0.1], 103 | wspace=0.2, 104 | hspace=0.0, 105 | ) 106 | 107 | 108 | # -- collect peak frequency information 109 | data = [] 110 | for i_sub, (participant, exp) in enumerate(zip(participants, experiments)): 111 | participant_id = "%s_%s" % (participant, exp) 112 | 113 | df1 = df2[df2.participant == participant] 114 | peaks = helper.get_participant_peaks(participant, exp) 115 | 116 | for peak in peaks: 117 | 118 | df = pd.read_csv( 119 | "%s/sources_%s_peak_%.2f.csv" % (results_dir, participant_id, peak) 120 | ) 121 | df = df[df.snr > snr_threshold] 122 | df = df[df.freq < max_freq] 123 | nr_components = np.min([max_nr_components, len(df)]) 124 | 125 | for i_comp in range(nr_components): 126 | location = df.iloc[i_comp][["x", "y", "z"]].to_numpy() 127 | max_peak = df.iloc[i_comp]["freq"] 128 | snr = df.iloc[i_comp]["snr"] 129 | 130 | data.append((i_sub, max_peak, snr, location[1])) 131 | 132 | 133 | # -- plot pooled peak frequencies 134 | ax = plt.subplot(gs[0, 0]) 135 | yticks = [5, 10, 15, 20] 136 | 137 | # -- plot markers for all components 138 | data = np.array(data) 139 | all_peaks = data[:, 1] 140 | sc = ax.scatter( 141 | data[:, 0], 142 | data[:, 1], 143 | s=30 * data[:, 2], 144 | c=mpl.cm.plasma(norm(data[:, 3])), 145 | alpha=0.75, 146 | ) 147 | 148 | ax.set( 149 | xlabel="participant", 150 | xticklabels=[], 151 | xticks=range(0, N + 1, 1), 152 | yticks=yticks, 153 | ylim=(4.5, 20.5), 154 | ylabel="peak frequency [Hz]", 155 | ) 156 | 157 | 158 | ax2 = plt.subplot(gs[0, 2]) 159 | cb = plt.colorbar(mpl.cm.ScalarMappable(norm=norm, cmap=mpl.cm.plasma), cax=ax2) 160 | cb.set_label("component location along\nposterior-anterior axis") 161 | 162 | # -- histogram of pooled peak frequencies 163 | ax1 = plt.subplot(gs[0, 1], sharey=ax) 164 | for i in yticks: 165 | ax1.axhline(i, linestyle="dotted", color="red", alpha=0.2) 166 | bins = np.linspace(0, 25, 1) 167 | ax1.hist( 168 | np.array(all_peaks), 30, facecolor="k", alpha=0.5, orientation="horizontal" 169 | ) 170 | ax1.set_ylabel("peak frequency [Hz]") 171 | ax1.yaxis.set_ticks_position("left") 172 | ax1.yaxis.set_label_position("right") 173 | 174 | ax1.yaxis.tick_left() 175 | ax1.set_xticklabels([]) 176 | ax1.set(yticks=yticks, ylim=(4.5, 20.5)) 177 | 178 | fig.set_size_inches(7.5, 8.5) 179 | fig.savefig("../figures/fig5_variability.pdf", dpi=300) 180 | fig.show() 181 | -------------------------------------------------------------------------------- /code/fig_6b_example_waveform_shape.py: -------------------------------------------------------------------------------- 1 | """ Shows example of close-by rhythms with different waveform. 2 | """ 3 | import mne 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | import helper 7 | import ssd 8 | import matplotlib.gridspec as gridspec 9 | import pandas as pd 10 | import matplotlib.image as mpimg 11 | 12 | plt.ion() 13 | 14 | # -- load continuous data 15 | participant_id = "jc_fixation_pwrlaw" 16 | raw = mne.io.read_raw_fif("../working/%s_raw.fif" % participant_id) 17 | raw.load_data() 18 | raw.pick_types(ecog=True) 19 | 20 | peak = 9.45 21 | bin_width = 1.75 22 | filters, patterns = ssd.run_ssd(raw, peak, bin_width) 23 | nr_components = 2 24 | raw_ssd = ssd.apply_filters(raw, filters[:, :nr_components]) 25 | 26 | 27 | raw_ssd.filter(2, None) 28 | raw.filter(2, None) 29 | 30 | # -- pick the channels contributing most to the spatial patterns for comparison 31 | picks = [np.argmax(np.abs(patterns[:, 0])), np.argmax(np.abs(patterns[:, 1]))] 32 | raw2 = raw.copy().pick(picks) 33 | 34 | raw_ssd2 = raw_ssd.copy().pick_channels(raw_ssd.ch_names[:2]) 35 | raw_ssd2.add_channels([raw2]) 36 | raw2 = raw_ssd2.copy() 37 | 38 | # -- compute spectrum 39 | psd, freq = mne.time_frequency.psd_welch(raw2, fmin=1, fmax=45, n_fft=3000) 40 | 41 | fig = plt.figure() 42 | gs = gridspec.GridSpec( 43 | 2, 44 | 4, 45 | width_ratios=[3, 1, 0.9, 0.75], 46 | ) 47 | 48 | # -- plot time domain 49 | ax1 = plt.subplot(gs[0, 0]) 50 | 51 | colors = ["#CC2B1C", "#3C2C71", "k", "k"] 52 | raw3 = raw2.copy() 53 | raw3.filter(1, None) 54 | 55 | tmin = 37.25 56 | tmax = tmin + 2.5 57 | raw3.crop(tmin, tmax) 58 | 59 | labels = [ 60 | "comp.1", 61 | "comp.2", 62 | "e1", 63 | "e2", 64 | ] 65 | 66 | signs = [1, 1, 1, 1, 1] 67 | for i in range(len(raw3.ch_names)): 68 | signal = signs[i] * raw3._data[i] 69 | signal = signal / np.ptp(signal) 70 | ax1.plot(raw3.times, signal - 1.05 * i, color=colors[i], lw=1) 71 | ax1.text(-0.05, -1 * i, labels[i], color=colors[i], ha="right", va="center") 72 | 73 | ax1.set(xlim=(0, 1.75), xlabel="time [s]", yticks=[], xticks=[0, 1, 2]) 74 | 75 | # -- plot spectrum 76 | ax1 = plt.subplot(gs[0, 1:3]) 77 | 78 | for i in range(len(raw3.ch_names)): 79 | ax1.semilogy(freq, psd[i].T, color=colors[i], lw=1) 80 | ax1.set( 81 | xlim=(1, 45), 82 | xlabel="frequency [Hz]", 83 | ) 84 | ax1.axvspan( 85 | peak - bin_width, peak + bin_width, color="gray", alpha=0.25, zorder=-3 86 | ) 87 | 88 | # -- plot peak frequency markers 89 | ax1.axvline(peak, color="tab:green", lw=1, zorder=-2) 90 | ax1.axvline(2 * peak, color="tab:green", lw=1, zorder=-2) 91 | ax1.axvline(3 * peak, color="tab:green", lw=1, zorder=-2) 92 | 93 | 94 | # -- plot participant summary 95 | ax1 = plt.subplot(gs[1, 0]) 96 | 97 | df = pd.read_csv("../csv/selected_datasets.csv") 98 | df = df[df.is_rest] 99 | nr_participants = len(df) 100 | results_folder = "../results/bursts/" 101 | 102 | counter = 0 103 | nr_bursts = [] 104 | asymmetry = [] 105 | for i_sub, (participant, exp) in enumerate(zip(df.participant, df.experiment)): 106 | 107 | participant_id = "%s_%s" % (participant, exp) 108 | peaks = helper.get_participant_peaks(participant, exp) 109 | 110 | for peak1 in peaks: 111 | df_file_name = "%s/bursts_%s_peak_%s.csv" % ( 112 | results_folder, 113 | participant_id, 114 | peak1, 115 | ) 116 | 117 | df = pd.read_csv(df_file_name) 118 | df = df.set_index("feature") 119 | 120 | if df.value["nr_bursts"] == 0: 121 | continue 122 | 123 | df = df.T 124 | frequency = 1000 / df.period 125 | if np.any(frequency < 5): 126 | continue 127 | ax1.plot( 128 | frequency, 129 | 2 * np.abs(df.time_ptsym - 0.5), 130 | ".", 131 | markeredgecolor="w", 132 | markerfacecolor="k", 133 | markersize=12, 134 | ) 135 | 136 | nr_bursts.append(df["nr_bursts"].to_list()) 137 | asymmetry.append(2 * np.abs(df.time_ptsym.to_list()[0] - 0.5)) 138 | 139 | counter += 1 140 | 141 | ax1.set( 142 | xlabel="peak frequency [Hz]", 143 | ylabel="peak-trough asymmetry", 144 | xlim=(4.5, 20.5), 145 | xticks=(range(5, 21, 5)), 146 | ) 147 | 148 | # -- asymmetries across 3d brain 149 | ax1 = plt.subplot(gs[1, 1:]) 150 | plot_filename = "../figures/3dbrain_asymmetry.png" 151 | img = mpimg.imread(plot_filename) 152 | 153 | ax1.imshow(img) 154 | ax1.set_xlim(25, 1000) 155 | ax1.set_ylim(725, 0) 156 | ax1.set(yticks=[], xticks=[]) 157 | ax1.axis("off") 158 | 159 | peaks_n = np.arange(0, 0.21, 0.01) 160 | N = len(peaks_n) 161 | cmap = [plt.cm.plasma(i) for i in np.linspace(0.2, 1, N)] 162 | 163 | 164 | fig2 = plt.figure() 165 | sc = plt.scatter( 166 | range(N), range(N), c=peaks_n, s=5, vmin=0, vmax=0.2, cmap=plt.cm.plasma 167 | ) 168 | plt.show() 169 | fig2.show() 170 | 171 | cb = plt.colorbar( 172 | sc, ax=ax1, orientation="horizontal", fraction=0.05, shrink=0.8, pad=0.0 173 | ) 174 | cb.set_label("peak-trough asymmetry") 175 | plt.close(fig2) 176 | 177 | 178 | # -- plot patterns 179 | topo_size = 0.18 180 | 181 | for i in range(2): 182 | 183 | ax = plt.Axes(fig, rect=[0.8, 0.77 - i * 0.2, topo_size, topo_size]) 184 | ax1 = fig.add_axes(ax) 185 | 186 | helper.make_topoplot( 187 | signs[i] * patterns[:, i], 188 | raw.info, 189 | ax1, 190 | picks=[picks[i]], 191 | pick_color=["g"], 192 | cmap="RdBu_r", 193 | hemisphere="left", 194 | plot_head=False, 195 | ) 196 | 197 | 198 | fig.set_size_inches(7.5, 6) 199 | fig.savefig("../figures/fig6_example_waveform.pdf", dpi=300) 200 | fig.show() 201 | -------------------------------------------------------------------------------- /code/fig_2_example_ecog.py: -------------------------------------------------------------------------------- 1 | """ This figure shows an example of SSD application for one participant. 2 | """ 3 | import mne 4 | import numpy as np 5 | import helper 6 | import ssd 7 | import matplotlib.pyplot as plt 8 | import matplotlib.gridspec as gridspec 9 | 10 | 11 | plt.ion() 12 | exp_id = "motor_basic" 13 | participant = "ug" 14 | 15 | # -- load continuous data 16 | file_name = "../working/%s_%s_raw.fif" % (participant, exp_id) 17 | raw = mne.io.read_raw_fif(file_name, preload=True) 18 | raw.pick_types(ecog=True) 19 | raw_org = raw.copy() 20 | 21 | # -- apply SSD and compute SNR for all components 22 | bin_width = 1.2 23 | peak = 9.46 24 | nr_components = 3 25 | 26 | filters, patterns = ssd.run_ssd(raw_org, peak, bin_width) 27 | raw_ssd = ssd.apply_filters(raw_org, filters) 28 | 29 | # -- SSD is polarity invariant, align to electrode signals by visual inspection 30 | signs = [1, -1, -1] 31 | for i_s, sign in enumerate(signs): 32 | patterns[:, i_s] *= sign 33 | raw_ssd._data[i_s] *= sign 34 | 35 | SNR_ssd = helper.get_SNR(raw_ssd, freq=[peak - 2, peak + 2]) 36 | raw_ssd.pick(range(nr_components)) 37 | 38 | # -- select electrodes and compute SNR for the selected peak frequency 39 | ch_names = ["ecog16", "ecog10", "ecog20"] 40 | picks = mne.pick_channels(raw.ch_names, ch_names, ordered=True) 41 | raw.pick(picks) 42 | SNR_electrodes = helper.get_SNR(raw, freq=[peak - 2, peak + 2]) 43 | 44 | # -- create figure 45 | fig = plt.figure() 46 | outer_grid = gridspec.GridSpec(2, 1, height_ratios=[2.5, 1]) 47 | top_cell = outer_grid[0, :] 48 | bottom_cell = outer_grid[1, :] 49 | gs = gridspec.GridSpecFromSubplotSpec( 50 | 2, 3, top_cell, width_ratios=[1.25, 2.5, 1] 51 | ) 52 | 53 | # -- plot electrode PSD + SNR 54 | ax1 = plt.subplot(gs[0, 0]) 55 | cmap1 = ["#2d004f", "#254f00", "#000000"] 56 | helper.plot_psd( 57 | ax1, 58 | raw, 59 | cmap=cmap1, 60 | SNR=SNR_electrodes, 61 | peak=peak, 62 | bin_width=bin_width, 63 | ) 64 | 65 | # -- plot PSD for SSD component 66 | ax1 = plt.subplot(gs[1, 0]) 67 | cmap = [plt.cm.viridis(i) for i in np.linspace(0.2, 1, 4)] 68 | 69 | helper.plot_psd( 70 | ax1, raw_ssd, cmap=cmap, SNR=SNR_ssd, peak=peak, bin_width=bin_width 71 | ) 72 | 73 | # -- plot time domain signals 74 | tmin = 4 75 | tmax = tmin + 2 76 | 77 | raw_ssd.filter(2, None) 78 | raw.filter(2, None) 79 | 80 | raw.crop(tmin, tmax) 81 | raw_ssd.crop(tmin, tmax) 82 | 83 | # -- plot electrode signals 84 | ax1 = plt.subplot(gs[0, 1]) 85 | helper.plot_timeseries(ax1, raw, cmap=cmap1, label="") 86 | ax1.set_xlabel("time [ms]") 87 | 88 | 89 | # -- pattern coefficients 90 | ax_cont = plt.subplot(gs[0, 2]) 91 | 92 | contributions = patterns[picks, :nr_components] 93 | vmax = np.max(np.abs(contributions)) 94 | ax_cont.imshow(contributions, cmap="RdBu_r", vmax=vmax, vmin=-vmax) 95 | ax_cont.set(yticks=[]) 96 | ax_cont.set_xticks(range(nr_components)) 97 | ax_cont.set_xticklabels(["comp.%i" % (i + 1) for i in range(nr_components)]) 98 | 99 | 100 | for (j, i), label in np.ndenumerate(contributions): 101 | ax_cont.text(i, j, "%.2f" % label, ha="center", va="center") 102 | 103 | 104 | # -- plot SSD time series 105 | ax1 = plt.subplot(gs[1, 1]) 106 | helper.plot_timeseries(ax1, raw_ssd, cmap=cmap, label="") 107 | ax1.set_xlabel("time [ms]") 108 | for i_chan in range(nr_components): 109 | ax1.text(0, -i_chan + 0.35, "component %i" % (i_chan + 1)) 110 | 111 | 112 | # -- plot SNR for SSD components 113 | ax1 = plt.subplot(gs[1, 2]) 114 | ax1.plot(SNR_ssd, ".-", color="k", markeredgecolor="w", markersize=8) 115 | for i in range(nr_components): 116 | ax1.plot( 117 | i, SNR_ssd[i], ".", color=cmap[i], markeredgecolor="w", markersize=12 118 | ) 119 | ax1.set(xlabel="component number", ylabel="SNR [dB]") 120 | 121 | 122 | # -- plot filters 123 | gs2 = gridspec.GridSpecFromSubplotSpec(1, 7, bottom_cell) 124 | topo_size = 0.26 125 | 126 | for i in range(nr_components): 127 | 128 | ax = plt.Axes(fig, rect=[i * 0.18 + 0.35, 0.12, topo_size, topo_size]) 129 | ax1 = fig.add_axes(ax) 130 | 131 | im_filters = helper.make_topoplot( 132 | filters[:, i], 133 | raw_org.info, 134 | ax, 135 | plot_head=False, 136 | picks=picks, 137 | cmap="PiYG", 138 | pick_color=["dimgrey"], 139 | vmin=-0.75, 140 | vmax=0.75, 141 | ) 142 | 143 | ax1.set_ylim(-0.04, 0.04) 144 | ax1.set_xlim(-0.08, 0.08) 145 | 146 | # -- plot patterns 147 | for i in range(nr_components): 148 | 149 | ax = plt.Axes(fig, rect=[i * 0.18 + 0.35, -0.05, topo_size, topo_size]) 150 | ax1 = fig.add_axes(ax) 151 | 152 | im_patterns = helper.make_topoplot( 153 | patterns[:, i], 154 | raw_org.info, 155 | ax1, 156 | plot_head=False, 157 | picks=picks, 158 | cmap="RdBu_r", 159 | vmin=-1.5, 160 | vmax=1.5, 161 | pick_color=["dimgrey"], 162 | ) 163 | 164 | ax1.set_title("component %i" % (i + 1)) 165 | ax1.set_ylim(-0.04, 0.04) 166 | ax1.set_xlim(-0.08, 0.08) 167 | 168 | 169 | # -- filters & patterns colorbars 170 | ax = plt.Axes(fig, rect=[0.3, 0.225, 0.15, 0.025]) 171 | ax1 = fig.add_axes(ax) 172 | cb = plt.colorbar(im_filters, cax=ax1, orientation="horizontal") 173 | ax1.set_title("spatial filters") 174 | 175 | ax = plt.Axes(fig, rect=[0.3, 0.05, 0.15, 0.025]) 176 | ax1 = fig.add_axes(ax) 177 | cb = plt.colorbar(im_patterns, cax=ax1, orientation="horizontal") 178 | ax1.set_title("spatial patterns") 179 | 180 | # -- electrodes on topo head 181 | ax = plt.Axes(fig, rect=[0.0, 0.02, topo_size, topo_size]) 182 | ax1 = fig.add_axes(ax) 183 | mask = np.zeros((len(raw_org.ch_names)), dtype="bool") 184 | mask[picks] = True 185 | mne.viz.plot_topomap( 186 | np.zeros((len(raw_org.ch_names),)) + np.nan, 187 | raw_org.info, 188 | axes=ax1, 189 | mask=mask, 190 | ) 191 | 192 | fig.set_size_inches(7.5, 6) 193 | fig.savefig("../figures/fig2_example_ecog.pdf", dpi=300) 194 | fig.show() 195 | -------------------------------------------------------------------------------- /code/fig_S1_example_ecog_car_bipolar.py: -------------------------------------------------------------------------------- 1 | """ This figure shows examples of different spatial filters for 1 participant. 2 | 3 | """ 4 | import mne 5 | import numpy as np 6 | import helper 7 | import ssd 8 | import matplotlib.pyplot as plt 9 | import matplotlib.gridspec as gridspec 10 | 11 | 12 | def plot_timeseries( 13 | ax1, raw, cmap=["#2d004f", "#254f00", "#000000"], label=None 14 | ): 15 | 16 | nr_channels = len(raw.ch_names) 17 | for i_pick in range(nr_channels): 18 | signal = raw._data[i_pick] 19 | signal = signal / np.ptp(signal) 20 | ax1.plot(raw.times, signal - i_pick, color=cmap[i_pick], lw=1) 21 | 22 | ymin = -nr_channels + 0.25 23 | ymax = 0.45 24 | ax1.set( 25 | xlim=(raw.times[0], raw.times[-1]), 26 | xticks=np.arange(0, 2.01, 0.5), 27 | yticks=[], 28 | ylim=(ymin, ymax), 29 | title=label, 30 | ) 31 | 32 | 33 | def plot_contribution(ax, contributions): 34 | 35 | vmax = np.max(np.abs(contributions)) 36 | 37 | ax.imshow(contributions, cmap="RdBu_r", vmax=vmax, vmin=-vmax) 38 | ax.plot(np.arange(-0.5, 3, 1), 0.49 * np.ones((4,)), color="k", lw=1) 39 | ax.plot(np.arange(-0.5, 3, 1), 1.49 * np.ones((4,)), color="k", lw=1) 40 | ax.set(yticks=[]) 41 | ax.set_xticks([0, 1, 2]) 42 | ax.xaxis.set_ticks_position("bottom") 43 | ax.set_xticklabels(["comp.%i" % (i + 1) for i in range(3)], rotation=45) 44 | 45 | for i in range(3): 46 | ax.get_xticklabels()[i].set_color(cmap[i]) 47 | 48 | for (j, i), label in np.ndenumerate(contributions): 49 | ax.text(i, j, "%.2f" % label, ha="center", va="center") 50 | 51 | 52 | # -- load continuous data 53 | exp_id = "motor_basic" 54 | participant = "ug" 55 | file_name = "../working/%s_%s_raw.fif" % (participant, exp_id) 56 | raw = mne.io.read_raw_fif(file_name, preload=True) 57 | raw.pick_types(ecog=True) 58 | 59 | # -- set parameters 60 | bin_width = 1.2 61 | peak = 9.46 62 | nr_channels = len(raw.ch_names) 63 | nr_seconds = 3 64 | chans_to_plot = 3 65 | n_fft = int(nr_seconds * raw.info["sfreq"]) 66 | tmin = 4 67 | tmax = tmin + 2 68 | 69 | 70 | # -- create plot 71 | fig = plt.figure() 72 | gs = gridspec.GridSpec( 73 | 4, 74 | 3, 75 | width_ratios=[1.25, 2.5, 1], 76 | ) 77 | 78 | # -- apply SSD 79 | filters, patterns_ssd = ssd.run_ssd(raw, peak, bin_width) 80 | nr_components = 3 81 | raw_ssd = ssd.apply_filters(raw, filters[:, :nr_components]) 82 | SNR_ssd = helper.get_SNR(raw_ssd) 83 | 84 | signs = [1, -1, -1] 85 | for i_s, sign in enumerate(signs): 86 | raw_ssd._data[i_s] *= sign 87 | patterns_ssd[:, i_s] *= sign 88 | 89 | 90 | cmap = [plt.cm.viridis(i) for i in np.linspace(0.2, 1, 4)] 91 | ax1 = plt.subplot(gs[3, 0]) 92 | helper.plot_psd( 93 | ax1, raw_ssd, cmap=cmap, SNR=SNR_ssd, peak=peak, bin_width=bin_width 94 | ) 95 | 96 | ax1 = plt.subplot(gs[3, 1]) 97 | raw_ssd.crop(tmin, tmax) 98 | plot_timeseries(ax1, raw_ssd, cmap=cmap, label="SSD") 99 | ax1.set_xlabel("time [ms]") 100 | 101 | # -- make bipolar anterior to posterior 102 | rows = [ 103 | [0, 1, 2, 3, 4], 104 | [5, 6, 7, 8, 9], 105 | [10, 11, 12, 13, 14], 106 | [15, 16, 17, 18, 19], 107 | [20, 21, 22, 23, 24], 108 | ] 109 | 110 | 111 | raw_AP, filters_AP, elecs = helper.create_bipolar_derivation( 112 | raw, rows, prefix="AP" 113 | ) 114 | SNR_AP = helper.get_SNR(raw_AP) 115 | 116 | picks = np.argsort(SNR_AP)[-chans_to_plot:][::-1] 117 | SNR_AP = SNR_AP[picks] 118 | elecs = elecs[picks] 119 | raw_AP.pick(list(picks)) 120 | 121 | 122 | # -- plot power spectrum 123 | cmap = ["#470509", "#76181E", "#A6373F"] 124 | ax1 = plt.subplot(gs[0, 0]) 125 | helper.helper.plot_psd( 126 | ax1, raw_AP, SNR=SNR_AP, peak=peak, cmap=cmap, bin_width=bin_width 127 | ) 128 | 129 | ax1 = plt.subplot(gs[0, 1]) 130 | raw_AP.crop(tmin, tmax) 131 | plot_timeseries(ax1, raw_AP, label="bipolar posterior-anterior", cmap=cmap) 132 | 133 | ax1 = plt.subplot(gs[0, 2]) 134 | helper.make_topoplot( 135 | np.zeros( 136 | (nr_channels,), 137 | ), 138 | raw.info, 139 | ax=ax1, 140 | picks=elecs, 141 | cmap="Greys", 142 | plot_head=False, 143 | pick_color=cmap, 144 | ) 145 | 146 | 147 | # -- make bipolar medial - lateral 148 | rows = [ 149 | [0, 5, 10, 15, 20], 150 | [1, 6, 11, 16, 21], 151 | [2, 7, 12, 17, 22], 152 | [3, 8, 13, 18, 23], 153 | [4, 9, 14, 19, 24], 154 | ] 155 | 156 | 157 | raw_ML, filters_ML, elecs = helper.create_bipolar_derivation( 158 | raw, rows, prefix="ML" 159 | ) 160 | SNR_ML = helper.get_SNR(raw_ML) 161 | 162 | picks = np.argsort(SNR_ML)[-chans_to_plot:][::-1] 163 | SNR_ML = SNR_ML[picks] 164 | elecs = elecs[picks] 165 | 166 | 167 | raw_ML.pick(list(picks)) 168 | 169 | cmap = ["#0E1746", "#313B74", "#49548B"] 170 | 171 | ax1 = plt.subplot(gs[1, 0]) 172 | helper.plot_psd( 173 | ax1, raw_ML, SNR=SNR_ML, peak=peak, cmap=cmap, bin_width=bin_width 174 | ) 175 | 176 | ax1 = plt.subplot(gs[1, 1]) 177 | raw_ML.crop(tmin, tmax) 178 | plot_timeseries(ax1, raw_ML, label="bipolar medial-lateral", cmap=cmap) 179 | 180 | # -- highlight electrodes 181 | ax1 = plt.subplot(gs[1, 2]) 182 | helper.make_topoplot( 183 | np.zeros( 184 | (nr_channels,), 185 | ), 186 | raw.info, 187 | ax=ax1, 188 | picks=elecs, 189 | cmap="Greys", 190 | plot_head=False, 191 | pick_color=cmap, 192 | ) 193 | 194 | # -- common average reference 195 | 196 | filters_car = np.zeros((nr_channels, nr_channels)) - 1 / nr_channels 197 | for i in range(nr_channels): 198 | filters_car[i, i] = 1 199 | 200 | raw_car = ssd.apply_filters(raw, filters_car, prefix="car") 201 | SNR_car = helper.get_SNR(raw_car) 202 | 203 | picks = np.argsort(SNR_car)[-chans_to_plot:][::-1] 204 | SNR_car = SNR_car[picks] 205 | raw_car.pick(picks) 206 | 207 | ax1 = plt.subplot(gs[2, 0]) 208 | cmap = ["#034500", "#156711", "#5AAC56"] 209 | helper.plot_psd( 210 | ax1, raw_car, SNR=SNR_car, peak=peak, cmap=cmap, bin_width=bin_width 211 | ) 212 | 213 | ax1 = plt.subplot(gs[2, 1]) 214 | raw_car.crop(tmin, tmax) 215 | plot_timeseries(ax1, raw_car, label="common average", cmap=cmap) 216 | ax1.axvspan(0.75, 1.5, ymin=0.66, ymax=0.99, edgecolor="r", facecolor=None) 217 | ax1 = plt.subplot(gs[2, 2]) 218 | helper.make_topoplot( 219 | np.zeros( 220 | (nr_channels,), 221 | ), 222 | raw.info, 223 | ax=ax1, 224 | picks=picks, 225 | cmap="Greys", 226 | plot_head=False, 227 | pick_color=cmap, 228 | ) 229 | 230 | 231 | fig.set_size_inches(7.5, 7.5) 232 | fig.savefig("../figures/fig_S1_example_car.pdf", dpi=300) 233 | fig.show() 234 | -------------------------------------------------------------------------------- /code/fig_S2_example_motor_task.py: -------------------------------------------------------------------------------- 1 | """ This figure shows an example of SSD referenced activity for a task. 2 | """ 3 | import mne 4 | import numpy as np 5 | import ssd 6 | import matplotlib.pyplot as plt 7 | import matplotlib.gridspec as gridspec 8 | from bycycle.features import compute_features 9 | 10 | plt.ion() 11 | 12 | # -- load continuous data 13 | exp_id = "motor_basic" 14 | participant = "ug" 15 | file_name = "../working/%s_%s_raw.fif" % (participant, exp_id) 16 | raw = mne.io.read_raw_fif(file_name, preload=True) 17 | raw.pick_types(ecog=True) 18 | 19 | # -- get common average referenced activity 20 | nr_channels = len(raw.ch_names) 21 | filters_car = np.zeros((nr_channels, nr_channels)) - 1 / nr_channels 22 | for i in range(nr_channels): 23 | filters_car[i, i] = 1 24 | raw_car = ssd.apply_filters(raw, filters_car, prefix="car") 25 | 26 | # -- compute SSD 27 | bin_width = 1.2 28 | peak = 9.46 29 | filters, patterns = ssd.run_ssd(raw, peak, bin_width) 30 | patterns_car = filters_car.T @ patterns 31 | 32 | # -- get SSS referenced signal 33 | nr_components = 3 34 | raw_ssd = ssd.apply_filters(raw, filters[:, :nr_components]) 35 | 36 | # -- detect bursts with bycycle 37 | picks = [np.argsort(np.abs(patterns_car[:, 0]))[-1]] 38 | nr_trials = 15 39 | raw_car.pick(picks) 40 | 41 | osc_param = { 42 | "amplitude_fraction_threshold": 0.5, 43 | "amplitude_consistency_threshold": 0.5, 44 | "period_consistency_threshold": 0.5, 45 | "monotonicity_threshold": 0.5, 46 | "N_cycles_min": 3, 47 | } 48 | 49 | bins = np.linspace(0, 1, 21) 50 | bandwidth = (peak - 2, peak + 2) 51 | Fs = int(raw.info["sfreq"]) 52 | 53 | 54 | raw_car.filter(3, None) 55 | raw_ssd.filter(3, None) 56 | 57 | i_comp = 0 58 | df_ssd = compute_features( 59 | raw_ssd._data[i_comp], 60 | Fs, 61 | f_range=bandwidth, 62 | burst_detection_kwargs=osc_param, 63 | center_extrema="P", 64 | ) 65 | df_ssd = df_ssd[df_ssd.is_burst] 66 | 67 | i = 0 68 | df = compute_features( 69 | raw_car._data[i], 70 | Fs, 71 | f_range=bandwidth, 72 | burst_detection_kwargs=osc_param, 73 | center_extrema="P", 74 | ) 75 | df = df[df.is_burst] 76 | 77 | 78 | # -- cut continuous signal into epochs 79 | events, event_id = mne.events_from_annotations(raw) 80 | tmin = -1.5 81 | tmax = 2.5 82 | 83 | colors = plt.cm.tab20b(np.linspace(0, 1, 20)) 84 | 85 | nr_trials = 15 86 | epochs = mne.Epochs(raw_ssd, events, tmin=tmin, tmax=tmax) 87 | epochs.load_data() 88 | 89 | # -- create figure 90 | fig = plt.figure() 91 | gs = gridspec.GridSpec(2, 2) 92 | 93 | labels = ["hand movement\nSSD", "tongue movement\nSSD"] 94 | for i_type, type in enumerate(["1", "2"]): 95 | counter = 0 96 | ax = plt.subplot(gs[0, i_type]) 97 | epochs1 = epochs[type] 98 | 99 | idx = events[:, 2] == int(type) 100 | events1 = events[idx] 101 | 102 | for i in range(nr_trials): 103 | 104 | sample = events1[i, 0] 105 | 106 | for ii in range(1): 107 | 108 | data = epochs1._data[i, ii] 109 | data = data / np.ptp(data) 110 | ax.plot( 111 | epochs.times, data - counter, color=colors[2 * i_type], lw=0.6 112 | ) 113 | 114 | idx_trial = (df_ssd["sample_peak"] > sample + tmin * Fs) & ( 115 | df_ssd["sample_peak"] < sample + tmax * Fs 116 | ) 117 | 118 | df1 = df_ssd[idx_trial] 119 | 120 | starts = raw.times[df1.sample_last_trough] - raw.times[sample] 121 | ends = raw.times[df1.sample_next_trough] - raw.times[sample] 122 | 123 | for iii in range(len(df1)): 124 | ax.fill( 125 | [starts[iii], starts[iii], ends[iii], ends[iii]], 126 | [ 127 | -counter - 0.5, 128 | -counter + 0.5, 129 | -counter + 0.5, 130 | -counter - 0.5, 131 | ], 132 | color=colors[2 * i_type], 133 | alpha=0.2, 134 | ) 135 | 136 | counter -= 1 137 | ax.axvline(0, color="k") 138 | ax.set( 139 | xlim=(tmin, tmax), 140 | title=labels[i_type], 141 | xlabel="time relative to movement cue [s]", 142 | ylabel="trial number", 143 | yticks=np.arange(nr_trials), 144 | yticklabels=np.arange(nr_trials)[::-1] + 1, 145 | ) 146 | 147 | ax.set_ylim(-0.5, nr_trials) 148 | 149 | 150 | # -- plot common average referenced signal 151 | epochs = mne.Epochs(raw_car, events, tmin=tmin, tmax=tmax) 152 | epochs.load_data() 153 | labels = [ 154 | "hand movement\ncommon average reference", 155 | "tongue movement\ncommon average reference", 156 | ] 157 | 158 | for i_type, type in enumerate(["1", "2"]): 159 | counter = 0 160 | ax = plt.subplot(gs[1, i_type]) 161 | epochs1 = epochs[type] 162 | 163 | idx = events[:, 2] == int(type) 164 | events1 = events[idx] 165 | 166 | for i in range(nr_trials): 167 | 168 | sample = events1[i, 0] 169 | 170 | for ii in range(1): 171 | data = epochs1._data[i, ii] 172 | data = data / np.ptp(data) 173 | ax.plot( 174 | epochs.times, 175 | data - counter, 176 | color=colors[2 * i_type + 12], 177 | lw=0.6, 178 | ) 179 | 180 | idx_trial = (df["sample_peak"] > sample + tmin * Fs) & ( 181 | df["sample_peak"] < sample + tmax * Fs 182 | ) 183 | 184 | df1 = df[idx_trial] 185 | 186 | starts = raw.times[df1.sample_last_trough] - raw.times[sample] 187 | ends = raw.times[df1.sample_next_trough] - raw.times[sample] 188 | for iii in range(len(df1)): 189 | ax.fill( 190 | [starts[iii], starts[iii], ends[iii], ends[iii]], 191 | [ 192 | -counter - 0.5, 193 | -counter + 0.5, 194 | -counter + 0.5, 195 | -counter - 0.5, 196 | ], 197 | color=colors[2 * i_type + 12], 198 | alpha=0.2, 199 | ) 200 | 201 | counter -= 1 202 | ax.axvline(0, color="k") 203 | ax.set( 204 | xlim=(tmin, tmax), 205 | yticks=np.arange(nr_trials), 206 | yticklabels=np.arange(nr_trials)[::-1] + 1, 207 | title=labels[i_type], 208 | xlabel="time relative to movement cue [s]", 209 | ylabel="trial number", 210 | ) 211 | ax.set_ylim(-0.5, nr_trials) 212 | 213 | 214 | fig.set_size_inches(7.5, 8) 215 | fig.savefig("../figures/fig_S2_bursts_example.pdf", dpi=300) 216 | fig.show() 217 | -------------------------------------------------------------------------------- /code/ssd.py: -------------------------------------------------------------------------------- 1 | """ Functions to compute Spatial-Spectral Decompostion (SSD). 2 | 3 | Reference 4 | --------- 5 | Nikulin VV, Nolte G, Curio G.: A novel method for reliable and fast 6 | extraction of neuronal EEG/MEG oscillations on the basis of 7 | spatio-spectral decomposition. Neuroimage. 2011 Apr 15;55(4):1528-35. 8 | doi: 10.1016/j.neuroimage.2011.01.057. Epub 2011 Jan 27. PMID: 21276858. 9 | 10 | """ 11 | 12 | import numpy as np 13 | from scipy.linalg import eig 14 | import mne 15 | 16 | 17 | def compute_ged(cov_signal, cov_noise): 18 | """Compute a generatlized eigenvalue decomposition maximizing principal 19 | directions spanned by the signal contribution while minimizing directions 20 | spanned by the noise contribution. 21 | 22 | Parameters 23 | ---------- 24 | cov_signal : array, 2-D 25 | Covariance matrix of the signal contribution. 26 | cov_noise : array, 2-D 27 | Covariance matrix of the noise contribution. 28 | 29 | Returns 30 | ------- 31 | filters : array 32 | SSD spatial filter matrix, columns are individual filters. 33 | 34 | """ 35 | 36 | nr_channels = cov_signal.shape[0] 37 | 38 | # check for rank-deficiency 39 | [lambda_val, filters] = eig(cov_signal) 40 | idx = np.argsort(lambda_val)[::-1] 41 | filters = np.real(filters[:, idx]) 42 | lambda_val = np.real(lambda_val[idx]) 43 | tol = lambda_val[0] * 1e-6 44 | r = np.sum(lambda_val > tol) 45 | 46 | # if rank smaller than nr_channels make expansion 47 | if r < nr_channels: 48 | print("Warning: Input data is not full rank") 49 | M = np.matmul(filters[:, :r], np.diag(lambda_val[:r] ** -0.5)) 50 | else: 51 | M = np.diag(np.ones((nr_channels,))) 52 | 53 | cov_signal_ex = (M.T @ cov_signal) @ M 54 | cov_noise_ex = (M.T @ cov_noise) @ M 55 | 56 | [lambda_val, filters] = eig(cov_signal_ex, cov_signal_ex + cov_noise_ex) 57 | 58 | # eigenvalues should be sorted by size already, but double checking 59 | idx = np.argsort(lambda_val)[::-1] 60 | filters = filters[:, idx] 61 | filters = np.matmul(M, filters) 62 | 63 | return filters 64 | 65 | 66 | def apply_filters(raw, filters, prefix="ssd"): 67 | """Apply spatial filters on continuous data. 68 | 69 | Parameters 70 | ---------- 71 | raw : instance of Raw 72 | Raw instance with signals to be spatially filtered. 73 | filters : array, 2-D 74 | Spatial filters as computed by SSD. 75 | prefix : string | None 76 | Prefix for renaming channels for disambiguation. If None: "ssd" 77 | is used. 78 | 79 | Returns 80 | ------- 81 | raw_projected : instance of Raw 82 | Raw instance with projected signals as traces. 83 | """ 84 | 85 | raw_projected = raw.copy() 86 | components = filters.T @ raw.get_data() 87 | nr_components = filters.shape[1] 88 | raw_projected._data = components 89 | 90 | ssd_channels = ["%s%i" % (prefix, i + 1) for i in range(nr_components)] 91 | mapping = dict(zip(raw.info["ch_names"], ssd_channels)) 92 | mne.channels.rename_channels(raw_projected.info, mapping) 93 | raw_projected.drop_channels(raw_projected.info["ch_names"][nr_components:]) 94 | 95 | return raw_projected 96 | 97 | 98 | def compute_patterns(cov_signal, filters): 99 | """Compute spatial patterns for a specific covariance matrix. 100 | 101 | Parameters 102 | ---------- 103 | cov_signal : array, 2-D 104 | Covariance matrix of the signal contribution. 105 | filters : array, 2-D 106 | Spatial filters as computed by SSD. 107 | Returns 108 | ------- 109 | patterns : array, 2-D 110 | Spatial patterns. 111 | """ 112 | 113 | top = cov_signal @ filters 114 | bottom = (filters.T @ cov_signal) @ filters 115 | patterns = top @ np.linalg.pinv(bottom) 116 | 117 | return patterns 118 | 119 | 120 | def run_ssd(raw, peak, band_width): 121 | """Wrapper for compute_ssd with standard settings for definining filters. 122 | 123 | Parameters 124 | ---------- 125 | raw : instance of Raw 126 | Raw instance with signals to be spatially filtered. 127 | peak : float 128 | Peak frequency of the desired signal contribution. 129 | band_width : float 130 | Spectral bandwidth for the desired signal contribution. 131 | 132 | Returns 133 | ------- 134 | filters : array, 2-D 135 | Spatial filters as computed by SSD, each column = 1 spatial filter. 136 | patterns : array, 2-D 137 | Spatial patterns, with each pattern being a column vector. 138 | """ 139 | 140 | signal_bp = [peak - band_width, peak + band_width] 141 | noise_bp = [peak - (band_width + 2), peak + (band_width + 2)] 142 | noise_bs = [peak - (band_width + 1), peak + (band_width + 1)] 143 | 144 | filters, patterns = compute_ssd(raw, signal_bp, noise_bp, noise_bs) 145 | 146 | return filters, patterns 147 | 148 | 149 | def compute_ssd(raw, signal_bp, noise_bp, noise_bs): 150 | """Compute SSD for a specific peak frequency. 151 | 152 | Parameters 153 | ---------- 154 | raw : instance of Raw 155 | Raw instance with signals to be spatially filtered. 156 | signal_bp : tuple 157 | Pass-band for defining the signal contribution. E.g. (8, 13) 158 | noise_bp : tuple 159 | Pass-band for defining the noise contribution. 160 | noise_bs : tuple 161 | Stop-band for defining the noise contribution. 162 | 163 | 164 | Returns 165 | ------- 166 | filters : array, 2-D 167 | Spatial filters as computed by SSD, each column = 1 spatial filter. 168 | patterns : array, 2-D 169 | Spatial patterns, with each pattern being a column vector. 170 | """ 171 | 172 | iir_params = dict(order=2, ftype="butter", output="sos") 173 | 174 | # bandpass filter for signal 175 | raw_signal = raw.copy().filter( 176 | l_freq=signal_bp[0], 177 | h_freq=signal_bp[1], 178 | method="iir", 179 | iir_params=iir_params, 180 | verbose=False, 181 | ) 182 | 183 | # bandpass filter 184 | raw_noise = raw.copy().filter( 185 | l_freq=noise_bp[0], 186 | h_freq=noise_bp[1], 187 | method="iir", 188 | iir_params=iir_params, 189 | verbose=False, 190 | ) 191 | 192 | # bandstop filter 193 | raw_noise = raw_noise.filter( 194 | l_freq=noise_bs[1], 195 | h_freq=noise_bs[0], 196 | method="iir", 197 | iir_params=iir_params, 198 | verbose=False, 199 | ) 200 | 201 | # compute covariance matrices for signal and noise contributions 202 | 203 | if raw_signal._data.ndim == 3: 204 | cov_signal = mne.compute_covariance(raw_signal, verbose=False).data 205 | cov_noise = mne.compute_covariance(raw_noise, verbose=False).data 206 | elif raw_signal._data.ndim == 2: 207 | cov_signal = np.cov(raw_signal._data) 208 | cov_noise = np.cov(raw_noise._data) 209 | 210 | # compute spatial filters 211 | filters = compute_ged(cov_signal, cov_noise) 212 | 213 | # compute spatial patterns 214 | patterns = compute_patterns(cov_signal, filters) 215 | 216 | return filters, patterns 217 | -------------------------------------------------------------------------------- /code/fig_4_spatial_spread.py: -------------------------------------------------------------------------------- 1 | """ Figure shows example of two close-by rhythms and illustrates spatial spread. 2 | """ 3 | import mne 4 | import numpy as np 5 | import helper 6 | import ssd 7 | import matplotlib.pyplot as plt 8 | import matplotlib.gridspec as gridspec 9 | import pandas as pd 10 | from scipy.spatial.distance import pdist, squareform 11 | 12 | plt.ion() 13 | 14 | # -- load continuous data 15 | participant_id = "wc_fixation_pwrlaw" 16 | raw = mne.io.read_raw_fif("../working/%s_raw.fif" % participant_id) 17 | raw.load_data() 18 | raw.pick_types(ecog=True) 19 | 20 | # -- compute & apply SSD spatial filters 21 | peak = 8.15 22 | bin_width = 2 23 | filters, patterns = ssd.run_ssd(raw, peak, bin_width) 24 | 25 | nr_components = 2 26 | raw_ssd = ssd.apply_filters(raw, filters[:, :nr_components]) 27 | picks = [np.argmax(np.abs(patterns[:, 0])), np.argmax(np.abs(patterns[:, 1]))] 28 | raw2 = raw.copy().pick(picks) 29 | raw2.add_channels([raw_ssd]) 30 | 31 | # -- compute spectrum 32 | psd, freq = mne.time_frequency.psd_welch(raw2, fmin=1, fmax=45, n_fft=3000) 33 | 34 | # -- create plot 35 | fig = plt.figure() 36 | outer_grid = gridspec.GridSpec(2, 1, height_ratios=[1, 2]) 37 | top_cell = outer_grid[0, :] 38 | gs = gridspec.GridSpecFromSubplotSpec(1, 3, top_cell, width_ratios=[1, 2, 1]) 39 | 40 | # -- plot time domain 41 | ax1 = plt.subplot(gs[0, 1], zorder=10) 42 | colors = ["k", "k", "#CC2B1C", "#3C2C71"] 43 | 44 | raw3 = raw2.copy() 45 | raw3.filter(1, None) 46 | 47 | tmin = 89.2 48 | tmax = tmin + 2 49 | raw3.crop(tmin, tmax) 50 | 51 | labels = ["electrode 1", "electrode 2", "component 1", "component 2"] 52 | 53 | for i in range(len(raw3.ch_names)): 54 | signal = raw3._data[i] 55 | signal = signal / np.ptp(signal) 56 | ax1.plot(raw3.times, signal - 1.2 * i, color=colors[i], lw=1, zorder=20) 57 | ax1.text(0.0, -1.2 * i - 0.75, labels[i], color=colors[i]) 58 | ax1.set(xlim=(0, 2), xlabel="time [s]", yticks=[], ylim=(-4.5, 0.65)) 59 | 60 | # -- plot spectrum 61 | ax1 = plt.subplot(gs[0, 2]) 62 | 63 | for i in range(len(raw3.ch_names)): 64 | ax1.semilogy(freq, psd[i].T, color=colors[i], lw=1) 65 | ax1.set(xlim=(1, 45), xlabel="frequency [Hz]") 66 | ax1.axvspan( 67 | peak - bin_width, peak + bin_width, color="gray", alpha=0.25, zorder=-3 68 | ) 69 | ax1.set(ylabel="log PSD [a.u.]", yticklabels=[]) 70 | ax1.yaxis.set_label_position("right") 71 | 72 | 73 | # -- plot patterns 74 | topo_size = 0.33 75 | coord_y = 0.68 76 | offset_x = 0 77 | 78 | for i in range(2): 79 | 80 | ax = plt.Axes( 81 | fig, 82 | rect=[offset_x + i * 0.12, coord_y, topo_size, topo_size], 83 | zorder=1 + 2 * i, 84 | ) 85 | ax1 = fig.add_axes(ax, zorder=1 + 2 * i) 86 | 87 | im_patterns = helper.make_topoplot( 88 | patterns[:, i], 89 | raw.info, 90 | ax1, 91 | picks=[picks[i]], 92 | pick_color=["g"], 93 | cmap="RdBu_r", 94 | hemisphere="left", 95 | plot_head=True, 96 | ) 97 | 98 | 99 | # -- compile patterns for calculating spatial spread 100 | df = pd.read_csv("../csv/selected_datasets.csv") 101 | df = df[df.is_rest] 102 | 103 | participants = df.participant 104 | experiments = df.experiment 105 | df = df.set_index("participant") 106 | 107 | 108 | nr_patterns = 1 109 | df2 = pd.DataFrame() 110 | 111 | for i_s, (participant, exp) in enumerate(zip(participants, experiments)): 112 | 113 | # load electrode positions 114 | participant_id = "%s_%s" % (participant, exp) 115 | raw = mne.io.read_raw_fif("../working/%s_raw.fif" % participant_id) 116 | raw.crop(0, 1) 117 | raw.load_data() 118 | raw.pick_types(ecog=True) 119 | raw = helper.reject_channels(raw, participant, exp) 120 | 121 | # load patterns for all peak frequencies 122 | peaks = helper.get_participant_peaks(participant, exp) 123 | 124 | for i_p, peak in enumerate(peaks): 125 | patterns, filters = helper.load_ssd(participant_id, peak) 126 | 127 | for idx in range(nr_patterns): 128 | pattern = patterns[:, idx] 129 | xyz = np.array([ich["loc"][:3] for ich in raw.info["chs"]]) 130 | distance = squareform(pdist(xyz, "cityblock")) 131 | 132 | # find maximum value & select distance to maximum 133 | i_max = np.argmax(np.abs(pattern)) 134 | dist_to_max = distance[i_max] 135 | 136 | # normalize pattern and find maxmium coefficient 137 | norm_pattern = np.abs(pattern / pattern[i_max]) 138 | idx_dist = (dist_to_max > 0) & (dist_to_max < 25) 139 | max_coefficent = np.sort(norm_pattern[idx_dist])[-1] 140 | 141 | # save in dataframe 142 | data = dict( 143 | spread=max_coefficent, 144 | participant=participant, 145 | pattern=pattern, 146 | peak=peak, 147 | ) 148 | df2 = df2.append(data, ignore_index=True) 149 | 150 | 151 | spreads = df2.spread.to_numpy() 152 | freqs = df2.peak.to_numpy() 153 | 154 | 155 | # -- make plot 156 | 157 | top_cell = outer_grid[1, :] 158 | gs = gridspec.GridSpecFromSubplotSpec( 159 | 1, 3, top_cell, width_ratios=[0.4, 1, 0.035] 160 | ) 161 | 162 | # -- plot spatial spread 163 | ax1 = plt.subplot(gs[0, 0]) 164 | ax1.plot(freqs, spreads, "k.", markersize=8, markeredgecolor="#DDDDDD") 165 | ax1.set( 166 | xlabel="frequency [Hz]", 167 | ylabel="normalized maximal spatial pattern" 168 | "\n" 169 | "coefficient of neighboring electrodes", 170 | xlim=(4.5, 21), 171 | xticks=[5, 10, 15, 20], 172 | ) 173 | ax1.set_aspect(30) 174 | 175 | gs1 = gridspec.GridSpecFromSubplotSpec(2, 3, gs[0, 1]) 176 | 177 | # -- plot some example topographies with large and small spread 178 | examples_large = (("gc", 12.3), ("jm", 5.4), ("h0", 24.1)) 179 | examples_small = (("rr", 12.7), ("mv", 13.5), ("hh", 7.5)) 180 | examples = [examples_large, examples_small] 181 | 182 | labels = [ 183 | "spatial spread over multiple electrodes", 184 | "spatial spread with one predominant maximum", 185 | ] 186 | 187 | for i_cond in range(len(examples)): 188 | 189 | examples1 = examples[i_cond] 190 | 191 | for i in range(len(examples1)): 192 | ax1 = plt.subplot(gs1[i_cond, i]) 193 | 194 | participant, peak = examples1[i] 195 | df1 = df2[df2.participant == participant] 196 | df1 = df1[df1.peak == peak] 197 | 198 | pattern = df1.iloc[0].pattern 199 | participant = df1.iloc[0].participant 200 | maxC = df1.iloc[0].spread 201 | peak = df1.iloc[0].peak 202 | 203 | idx_max = np.argmax(np.abs(pattern)) 204 | raw = mne.io.read_raw_fif( 205 | "../working/%s_fixation_pwrlaw_raw.fif" % participant 206 | ) 207 | raw.load_data() 208 | raw.pick_types(ecog=True) 209 | raw = helper.reject_channels(raw, participant, "fixation_pwrlaw") 210 | 211 | # plot normalized patterns 212 | pattern = np.abs(pattern) 213 | pattern = pattern / np.max(pattern) 214 | sc = helper.make_topoplot( 215 | pattern, 216 | raw.info, 217 | ax1, 218 | cmap="Reds", 219 | picks=[idx_max], 220 | pick_color=["g"], 221 | vmin=0, 222 | vmax=1, 223 | plot_head=None, 224 | size=40, 225 | ) 226 | xlim = ax1.get_xlim() 227 | ax1.set_xlim(xlim[0] - 0.005, xlim[1] + 0.005) 228 | ax1.set_ylabel("%.2f" % maxC) 229 | 230 | if i == 1: 231 | ax1.set_title(labels[i_cond]) 232 | 233 | 234 | ax1 = plt.subplot(gs[:, 2]) 235 | cb = plt.colorbar(sc, cax=ax1) 236 | cb.set_label("normalized spatial pattern coefficient") 237 | fig.set_size_inches(7.5, 5.5) 238 | fig.savefig("../figures/fig4_spatial_spread.pdf", dpi=300) 239 | fig.show() 240 | -------------------------------------------------------------------------------- /code/fig_3_example_seeg.py: -------------------------------------------------------------------------------- 1 | """ This figure shows examples where SSD improves SNR of oscillations in sEEG. 2 | """ 3 | import mne 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | import helper 7 | import ssd 8 | import matplotlib.gridspec as gridspec 9 | import fooof 10 | import pyvista as pv 11 | import matplotlib.image as mpimg 12 | 13 | 14 | def plot_seeg_3dbrain(raw): 15 | """Convenience function for plotting 3d brain and the 3 leads.""" 16 | 17 | # camera position for ventral view 18 | cpos = [ 19 | (192.1355480872916, -142.5172055994041, -261.47527261474386), 20 | (24.065850569725583, -19.416839467317363, 15.642167878335641), 21 | (0.20039110730958792, 0.9346693866417147, -0.29366058943283213), 22 | ] 23 | 24 | pv.set_plot_theme("document") 25 | plotter = pv.Plotter(off_screen=True, window_size=[350, 700]) 26 | cloud = helper.plot_3d_brain() 27 | actor = plotter.add_mesh(cloud, opacity=1) 28 | xyz_all = np.array([ich["loc"][:3] for ich in raw.info["chs"]]) 29 | picks_all = [range(8), range(8, 14), range(14, 19)] 30 | colors = ["purple", "#41A859", "dodgerblue"] 31 | 32 | # plot each lead in a different color 33 | for i, picks in enumerate(picks_all): 34 | ch_names = ["ecog%i" % i for i in picks] 35 | lead1 = raw.copy().pick_channels(ch_names) 36 | xyz = np.array([ich["loc"][:3] for ich in lead1.info["chs"]]) 37 | 38 | # slightly adjust coordinates to prevent electrodes from being invisible 39 | xyz[:, 0] += 4 40 | xyz[:, 2] -= 4 41 | 42 | electrodes = pv.PolyData(xyz) 43 | act_electrodes = plotter.add_mesh( 44 | electrodes, 45 | point_size=25, 46 | color=colors[i], 47 | render_points_as_spheres=True, 48 | ) 49 | 50 | xyz_all[:, 0] += 4 51 | xyz_all[:, 2] -= 4 52 | for ii, pick in enumerate(picks1): 53 | 54 | electrodes = pv.PolyData(xyz_all[pick]) 55 | act_electrodes = plotter.add_mesh( 56 | electrodes, 57 | point_size=40, 58 | color=colors_sel[ii], 59 | render_points_as_spheres=True, 60 | ) 61 | 62 | plotter.show( 63 | cpos=cpos, title=str(cpos), screenshot="../figures/3dbrain_seeg.png" 64 | ) 65 | plotter.screenshot( 66 | "../figures/3dbrain_seeg.png", transparent_background=True 67 | ) 68 | plotter.close() 69 | 70 | 71 | def plot_SNR(freq, psd, ax, color, x=5, y=51, peak=9.5, plot=True): 72 | """ Convenience function for extracting and plotting SNR. """ 73 | fg = fooof.FOOOF() 74 | fg.fit(freq, psd) 75 | peak_freq = fooof.analysis.get_band_peak_fm(fg, [peak - 1, peak + 1])[0] 76 | idx_max = np.argmin(np.abs(freq - peak_freq)) 77 | if plot: 78 | ax.plot( 79 | fg.freqs, 10 * fg._ap_fit, color=color, linestyle="--", alpha=0.8 80 | ) 81 | ax.plot( 82 | [peak_freq, peak_freq], 83 | [10 * fg._ap_fit[idx_max], 10 * np.log10(psd[idx_max])], 84 | lw=1, 85 | zorder=-3, 86 | color=color, 87 | ) 88 | SNR = 10 * np.log10(psd[idx_max]) - 10 * fg._ap_fit[idx_max] 89 | ax.text(x, y, "SNR=%.1f dB" % SNR, color=color) 90 | 91 | 92 | # -- set colors & PSD parameters 93 | plt.ion() 94 | nr_seconds = 3 95 | fmin = 2 96 | fmax = 55 97 | 98 | colors_bi = ["#000000", "#444444", "#888888"] 99 | colors_sel = ["purple", "#41A859", "dodgerblue"] 100 | colors_ssd = ["#CC2B1C", "#3C2C71"] 101 | 102 | # -- load continuous data 103 | participant_id = "ja_faces_basic" 104 | raw_file = "../working/%s_raw.fif" % participant_id 105 | raw = mne.io.read_raw_fif(raw_file) 106 | raw.load_data() 107 | raw.pick_types(ecog=True) 108 | 109 | # -- select 3 sEEG leads 110 | picks = range(19) 111 | ch_names = ["ecog%i" % i for i in picks] 112 | raw.pick_channels(ch_names) 113 | raw2 = raw.copy() 114 | 115 | ch_names_leads = ["ecog3", "ecog13", "ecog18"] 116 | picks1 = mne.pick_channels(raw2.ch_names, ch_names_leads, ordered=True) 117 | plot_seeg_3dbrain(raw) 118 | 119 | # -- apply SSD 120 | peak1 = 4.0 121 | bin_width1 = 1.25 122 | filters1, patterns = ssd.run_ssd(raw, peak1, bin_width1) 123 | 124 | peak2 = 8.15 125 | bin_width2 = 2.5 126 | filters2, patterns2 = ssd.run_ssd(raw, peak2, bin_width2) 127 | 128 | # combine top-SNR filters for each peak frequency into one matrix 129 | filters1[:, 1] = filters2[:, 0] 130 | patterns[:, 1] = patterns2[:, 0] 131 | raw_ssd = ssd.apply_filters(raw, filters1[:, :2]) 132 | raw_ssd.filter(1, None) 133 | 134 | picks = range(8) 135 | ch_names = ["ecog%i" % i for i in picks] 136 | lead1 = raw.copy().pick_channels(ch_names) 137 | lead1.set_eeg_reference("average") 138 | 139 | 140 | picks = range(8, 14) 141 | ch_names = ["ecog%i" % i for i in picks] 142 | lead2 = raw.copy().pick_channels(ch_names) 143 | lead2.set_eeg_reference("average") 144 | 145 | picks = range(14, 19) 146 | ch_names = ["ecog%i" % i for i in picks] 147 | lead3 = raw.copy().pick_channels(ch_names) 148 | lead3.set_eeg_reference("average") 149 | 150 | 151 | lead1.add_channels([lead2, lead3]) 152 | raw = lead1 153 | raw.filter(1, None) 154 | 155 | 156 | # -- apply bipolar filtering 157 | filters = np.zeros((len(raw.ch_names), 2)) 158 | filters[7, 0] = 1 159 | filters[8, 0] = -1 160 | filters[17, 1] = 1 161 | filters[18, 1] = -1 162 | raw_bipolar = ssd.apply_filters(raw2, filters, prefix="bipolar") 163 | 164 | 165 | # -- compute PSD for CAR, bipolar & SSD signals 166 | raw.pick_channels(ch_names_leads) 167 | n_fft = int(nr_seconds * raw.info["sfreq"]) 168 | 169 | psd, freq = mne.time_frequency.psd_welch(raw, fmin=fmin, fmax=fmax, n_fft=n_fft) 170 | 171 | psd_ssd, freq = mne.time_frequency.psd_welch( 172 | raw_ssd, fmin=fmin, fmax=fmax, n_fft=n_fft 173 | ) 174 | 175 | psd_bipolar, freq = mne.time_frequency.psd_welch( 176 | raw_bipolar, fmin=fmin, fmax=fmax, n_fft=n_fft 177 | ) 178 | 179 | # -- crop a time interval with some oscillations 180 | tmin = 143 181 | tmax = tmin + 2.5 182 | raw.crop(tmin, tmax) 183 | raw_ssd.crop(tmin, tmax) 184 | raw_bipolar.crop(tmin, tmax) 185 | 186 | # -- create plot 187 | fig, ax = plt.subplots(figsize=(7.5, 5)) 188 | gs = gridspec.GridSpec(3, 4) 189 | 190 | # -- plot brain 191 | ax = plt.subplot(gs[:2, 0]) 192 | 193 | img = mpimg.imread("../figures/3dbrain_seeg.png") 194 | ax.imshow(img) 195 | ax.axis("off") 196 | 197 | 198 | # collect all information in an unwieldy way 199 | cond = ( 200 | [ 201 | raw, 202 | psd, 203 | colors_sel, 204 | [True, True, False], 205 | ["CAR\ne1", "CAR\ne2", "CAR\ne3"], 206 | [peak1, peak2, peak1], 207 | ], 208 | [ 209 | raw_bipolar, 210 | psd_bipolar, 211 | colors_bi, 212 | [True, True], 213 | ["bipolar\ne1", "bipolar\ne2"], 214 | [peak2, peak1, peak1], 215 | ], 216 | [ 217 | raw_ssd, 218 | psd_ssd, 219 | colors_ssd, 220 | [True, True], 221 | [ 222 | "SSD component\npeak frequency= %.1f Hz" % peak1, 223 | "SSD component\npeak frequency= %.1f Hz" % peak2, 224 | ], 225 | [peak1, peak2], 226 | ], 227 | ) 228 | 229 | # -- plot time domain signals 230 | ax = plt.subplot(gs[:, 1:3]) 231 | 232 | counter = 0 233 | for i_cond in range(3): 234 | raw1, psd1, colors1, _, labels, _ = cond[i_cond] 235 | for i in range(len(raw1.ch_names)): 236 | signal = raw1._data[i] 237 | signal = signal / np.ptp(signal) 238 | ax.plot(raw.times, signal - counter, color=colors1[i], lw=1) 239 | ax.text( 240 | -0.1, 241 | -counter - 0.1, 242 | labels[i], 243 | va="center", 244 | ha="right", 245 | color=colors1[i], 246 | ) 247 | counter += 1 248 | counter += 0.5 249 | 250 | ax.set(xlim=(raw.times[0], raw.times[-1]), xlabel="time [s]", yticks=[]) 251 | 252 | # -- plot spectrum for all referencing types 253 | for i_cond in range(3): 254 | ax = plt.subplot(gs[i_cond, 3]) 255 | raw1, psd1, colors1, plot1, _, peaks = cond[i_cond] 256 | for i in range(len(raw1.ch_names)): 257 | ax.plot(freq, 10 * np.log10(psd1[i]).T, color=colors1[i], lw=1) 258 | plot_SNR( 259 | freq, 260 | psd1[i], 261 | ax, 262 | colors1[i], 263 | peak=peaks[i], 264 | x=12, 265 | y=10 * np.log10(psd1[0, 0]) - i * 5, 266 | plot=plot1[i], 267 | ) 268 | if i_cond == 2: 269 | ax.set(xlabel="frequency [Hz]") 270 | ax.axvspan( 271 | peak1 - bin_width1, 272 | peak1 + bin_width1, 273 | color=colors_ssd[0], 274 | alpha=0.25, 275 | zorder=-3, 276 | ) 277 | 278 | ax.axvspan( 279 | peak2 - bin_width2, 280 | peak2 + bin_width2, 281 | color=colors_ssd[1], 282 | alpha=0.25, 283 | zorder=-3, 284 | ) 285 | 286 | fig.savefig("../figures/fig3_example_sEEG.pdf", dpi=300) 287 | -------------------------------------------------------------------------------- /code/helper.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import ast 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | import scipy.io 6 | import helper 7 | import mne 8 | from mne.channels.layout import _find_topomap_coords 9 | from mne.viz.topomap import _make_head_outlines 10 | import pyvista as pv 11 | import fooof 12 | import ssd 13 | 14 | 15 | def make_topoplot( 16 | values, 17 | info, 18 | ax, 19 | vmin=None, 20 | vmax=None, 21 | plot_head=True, 22 | cmap="RdBu_r", 23 | size=30, 24 | hemisphere="all", 25 | picks=None, 26 | pick_color=["#2d004f", "#254f00", "#000000"], 27 | ): 28 | """Makes an ECoG topo plot with electrodes circles, without interpolation. 29 | Modified from MNE plot_topomap to plot electrodes without interpolation. 30 | 31 | Parameters 32 | ---------- 33 | values : array, 1-D 34 | Values to plot as color-coded circles. 35 | info : instance of Info 36 | The x/y-coordinates of the electrodes will be infered from this object. 37 | ax : instance of Axes 38 | The axes to plot to. 39 | vmin : float | None 40 | Lower bound of the color range. If None: - maximum absolute value. 41 | vmax : float | None 42 | Upper bounds of the color range. If None: maximum absolute value. 43 | plot_head : True | False 44 | Whether to plot the outline for the head. 45 | cmap : matplotlib colormap | None 46 | Colormap to use for values, if None, defaults to RdBu_r. 47 | size : int 48 | Size of electrode circles. 49 | picks : list | None 50 | Which electrodes should be highlighted with by drawing a thicker edge. 51 | pick_color : list 52 | Edgecolor for highlighted electrodes. 53 | hemisphere : string ("left", "right", "all") 54 | Restrict which hemisphere of head outlines coordinates to plot. 55 | 56 | Returns 57 | ------- 58 | sc : matplotlib PathCollection 59 | The colored electrode circles. 60 | """ 61 | 62 | pos = _find_topomap_coords(info, picks=None) 63 | sphere = np.array([0.0, 0.0, 0.0, 0.095]) 64 | outlines = _make_head_outlines( 65 | sphere=sphere, pos=pos, outlines="head", clip_origin=(0.0, 0.0) 66 | ) 67 | 68 | if plot_head: 69 | outlines_ = { 70 | k: v for k, v in outlines.items() if k not in ["patch", "mask_pos"] 71 | } 72 | for key, (x_coord, y_coord) in outlines_.items(): 73 | if hemisphere == "left": 74 | if type(x_coord) == np.ndarray: 75 | idx = x_coord <= 0 76 | x_coord = x_coord[idx] 77 | y_coord = y_coord[idx] 78 | ax.plot(x_coord, y_coord, color="k", linewidth=1, clip_on=False) 79 | elif hemisphere == "right": 80 | if type(x_coord) == np.ndarray: 81 | idx = x_coord >= 0 82 | x_coord = x_coord[idx] 83 | y_coord = y_coord[idx] 84 | ax.plot(x_coord, y_coord, color="k", linewidth=1, clip_on=False) 85 | else: 86 | ax.plot(x_coord, y_coord, color="k", linewidth=1, clip_on=False) 87 | 88 | if not (vmin) and not (vmax): 89 | vmin = -values.max() 90 | vmax = values.max() 91 | 92 | sc = ax.scatter( 93 | pos[:, 0], 94 | pos[:, 1], 95 | s=size, 96 | edgecolors="grey", 97 | c=values, 98 | vmin=vmin, 99 | vmax=vmax, 100 | cmap=plt.get_cmap(cmap), 101 | ) 102 | 103 | if np.any(picks): 104 | picks = np.array(picks) 105 | if picks.ndim > 0: 106 | if len(pick_color) == 1: 107 | pick_color = [pick_color] * len(picks) 108 | for i, idxx in enumerate(picks): 109 | ax.scatter( 110 | pos[idxx, 0], 111 | pos[idxx, 1], 112 | s=size, 113 | edgecolors=pick_color[i], 114 | facecolors="None", 115 | linewidths=1.5, 116 | c=None, 117 | vmin=vmin, 118 | vmax=vmax, 119 | cmap=plt.get_cmap(cmap), 120 | ) 121 | 122 | if picks.ndim == 2: 123 | if len(pick_color) == 1: 124 | pick_color = [pick_color] * len(picks) 125 | for i, idxx in enumerate(picks): 126 | ax.plot( 127 | pos[idxx, 0], 128 | pos[idxx, 1], 129 | linestyle="-", 130 | color=pick_color[i], 131 | linewidth=1.5, 132 | ) 133 | 134 | ax.axis("square") 135 | ax.axis("off") 136 | ax.xaxis.set_visible(False) 137 | ax.yaxis.set_visible(False) 138 | 139 | return sc 140 | 141 | 142 | def reject_channels(raw, participant, experiment): 143 | """Exclude electrodes for resting datasets according to csv-file. 144 | 145 | Parameters 146 | ---------- 147 | raw : instance of Raw 148 | Raw instance from where to remove channels. 149 | participant : string 150 | Participant ID. 151 | experiment : string 152 | Experiment ID, for instance "fixation_pwrlaw". 153 | 154 | Returns 155 | ------- 156 | raw : instance of Raw 157 | Raw instance with channels removed. 158 | 159 | """ 160 | 161 | df = pd.read_csv("../csv/selected_datasets.csv") 162 | df = df[df.participant == participant] 163 | df = df[df.experiment == experiment] 164 | drop_chan = df.electrodes.values[0] 165 | idx_chan = ast.literal_eval(drop_chan) 166 | if len(idx_chan) > 0: 167 | ch_names = np.array(raw.ch_names)[idx_chan] 168 | raw.drop_channels(ch_names) 169 | 170 | return raw 171 | 172 | 173 | def get_participant_peaks(participant, experiment, return_width=False): 174 | """Extract associated spectral peaks for a specific dataset. 175 | 176 | Parameters 177 | ---------- 178 | participant : string 179 | Participant ID. 180 | experiment : string 181 | Experiment ID, for instance "fixation_pwrlaw". 182 | Returns 183 | ------- 184 | peaks : array, 1-D 185 | The spectral peaks for a specific participant. 186 | """ 187 | 188 | df_ssd = pd.read_csv("../csv/retained_components.csv") 189 | df_ssd = df_ssd[df_ssd.participant == participant] 190 | df_ssd = df_ssd[df_ssd.experiment == experiment] 191 | peaks = df_ssd.frequency.to_numpy() 192 | bandwidth = df_ssd.bandwidth.to_numpy() 193 | 194 | if return_width: 195 | return peaks, bandwidth 196 | else: 197 | return peaks 198 | 199 | 200 | def compute_spatial_max(patterns, raw, plot=False): 201 | """Extracts the spatial maximum by interpolation. 202 | 203 | Parameters 204 | ---------- 205 | patterns : 206 | raw : instance of Raw 207 | Raw instance for extracting electrode positions 208 | plot : bool 209 | If True, plot the spatial patterns with maxima marked for verification. 210 | 211 | Returns 212 | ------- 213 | locations: array, 2-D 214 | The location of the spatial maximum for each pattern. 215 | """ 216 | electrodes = np.array([r["loc"][:3] for r in raw.info["chs"]]) 217 | 218 | hemisphere = helper.hemisphere(raw) 219 | if hemisphere == "left": 220 | electrodes[:, 0] = -electrodes[:, 0] 221 | 222 | x, y, z = electrodes[:, 0], electrodes[:, 1], electrodes[:, 2] 223 | ymin, ymax = min(y) - 15, max(y) + 15 224 | zmin, zmax = min(z) - 15, max(z) + 15 225 | yy, zz = np.mgrid[ymin:ymax:500j, zmin:zmax:500j] 226 | 227 | nr_patterns = patterns.shape[1] 228 | locations = np.zeros((nr_patterns, 3)) 229 | interp_all = np.zeros((nr_patterns, 500, 500)) 230 | for i in range(nr_patterns): 231 | idx_max = np.argmax(np.abs(patterns[:, i])) 232 | values = np.sign(patterns[idx_max, i]) * patterns[:, i] 233 | interp = scipy.interpolate.griddata( 234 | electrodes[:, 1:], values, xi=(yy, zz), method="cubic" 235 | ) 236 | interp_all[i] = interp 237 | interp[np.isnan(interp)] = -np.Inf 238 | a = np.argmin(np.abs(interp - np.max(interp))) 239 | coords = np.unravel_index(a, interp.shape) 240 | 241 | max_y = yy[coords[0], coords[1]] 242 | max_z = zz[coords[0], coords[1]] 243 | 244 | distance = np.sum( 245 | np.abs(electrodes[:, 1:] - np.array([max_y, max_z])), axis=1 246 | ) 247 | idx = np.argsort(distance) 248 | max_x = np.mean(electrodes[idx[:3], 0]) 249 | locations[i] = [max_x, max_y, max_z] 250 | 251 | if plot: 252 | fig, ax = plt.subplots(1, nr_patterns) 253 | for i in range(nr_patterns): 254 | ax1 = ax[i] 255 | ax1.imshow( 256 | interp_all[i].T, 257 | origin="lower", 258 | cmap=plt.cm.RdBu_r, 259 | extent=[ymin, ymax, zmin, zmax], 260 | aspect="auto", 261 | ) 262 | values = np.sign(patterns[idx_max, i]) * patterns[:, i] 263 | ax1.scatter(y, z, 10, c=values, marker=".") 264 | ax1.plot(locations[i, 1], locations[i, 2], "g.", markersize=10) 265 | 266 | fig.set_size_inches(15, 5) 267 | fig.show() 268 | 269 | return locations 270 | 271 | 272 | def load_ssd(participant_id, peak): 273 | """Load spatial filters and patterns for a specific dataset. 274 | 275 | Parameters 276 | ---------- 277 | participant : string 278 | Participant ID. 279 | peak : float 280 | frequency for which SSD filters and patterns should be loaded. 281 | Returns 282 | ------- 283 | patterns : array, 2-D 284 | Spatial patterns as computed with the signal covariance matrix. 285 | filters : array, 2-D 286 | Spatial filters as computed by SSD. 287 | """ 288 | 289 | file_name = "../results/ssd/ssd_%s_peak_%.2f.npy" % ( 290 | participant_id, 291 | peak, 292 | ) 293 | data = np.load(file_name, allow_pickle=True).item() 294 | filters = data["filters"] 295 | patterns = data["patterns"] 296 | 297 | return patterns, filters 298 | 299 | 300 | def hemisphere(raw): 301 | """Return hemisphere of electrodes based on coordinates. 302 | 303 | Parameters 304 | ---------- 305 | raw: instance of Raw 306 | 307 | Returns 308 | ------- 309 | hemisphere: string 310 | Specifies on which side the electrode grid is placed. 311 | """ 312 | xyz = np.array([ich["loc"][:3] for ich in raw.info["chs"]]) 313 | 314 | if np.all(xyz[:, 0] < 0): 315 | hemisphere = "left" 316 | elif np.all(xyz[:, 0] < 0): 317 | hemisphere = "right" 318 | else: 319 | hemisphere = "both" 320 | 321 | return hemisphere 322 | 323 | 324 | def get_camera_position(): 325 | """ Camera position for pyvista 3d brains.""" 326 | cpos = [ 327 | (250.6143195554263, 41.91225218786929, 88.70813933287698), 328 | (31.183101868789514, -7.437521450446045, 14.637308132215361), 329 | (-0.3152066263035874, -0.022473087557700527, 0.948756946256487), 330 | ] 331 | 332 | return cpos 333 | 334 | 335 | def plot_3d_brain(whichbrain="rightbrain"): 336 | """ Creates Pyvista object for one hemisphere.""" 337 | 338 | data = scipy.io.loadmat("../data/motor_basic/halfbrains.mat") 339 | pos = data[whichbrain].item()[0] 340 | tri = data[whichbrain].item()[1] - 1 341 | tri = tri.astype("int") 342 | faces = np.hstack((3 * np.ones((tri.shape[0], 1)), tri)) 343 | faces = faces.astype("int") 344 | brain_cloud = pv.PolyData(pos, faces) 345 | 346 | return brain_cloud 347 | 348 | 349 | def plot_electrodes(raw): 350 | """ Create Pyvista object of the electrodes of 1 participant.""" 351 | xyz = np.array([ich["loc"][:3] for ich in raw.info["chs"]]) 352 | hemisphere = helper.hemisphere(raw) 353 | if hemisphere == "left": 354 | xyz[:, 0] = -xyz[:, 0] 355 | electrodes = pv.PolyData(xyz) 356 | 357 | return electrodes 358 | 359 | 360 | def create_bipolar_derivation(raw, rows, prefix="bipolar"): 361 | """Convenience function for creating bipolar channels. 362 | 363 | Parameters 364 | ---------- 365 | raw : instance of Raw 366 | Raw instance containing traces for which bipolar derivation is taken. 367 | rows : list 368 | List of lists containing electrode indices, each list is treated 369 | separately. Bipolar channels are create between adjacent electrode 370 | entries. 371 | prefix : string 372 | Prefix for creating channel names. 373 | """ 374 | counter = 0 375 | nr_channels = len(raw.ch_names) 376 | nr_bipolar_channels = len(rows) * (len(rows[0]) - 1) 377 | elecs = np.zeros((nr_bipolar_channels, 2), dtype="int") 378 | filters = np.zeros((nr_channels, nr_bipolar_channels)) 379 | for row in rows: 380 | for i in range(len(row) - 1): 381 | filters[row[i], counter] = 1 382 | filters[row[i + 1], counter] = -1 383 | elecs[counter] = [row[i], row[i + 1]] 384 | counter += 1 385 | 386 | raw_bipolar = ssd.apply_filters(raw, filters, prefix=prefix) 387 | 388 | return raw_bipolar, filters, elecs 389 | 390 | 391 | def plot_timeseries( 392 | ax, raw, cmap=["#2d004f", "#254f00", "#000000"], label=None 393 | ): 394 | """Convenience function for plotting raw traces. 395 | 396 | Parameters 397 | ---------- 398 | ax : instance of Axes 399 | The axes to plot to. 400 | raw : instance of Raw 401 | Raw instance containing traces for which PSD is computed. 402 | cmap : list 403 | Colors for traces, specify for each channel. 404 | label : string 405 | Label to be plotted as title of axes. 406 | """ 407 | 408 | nr_channels = len(raw.ch_names) 409 | for i_pick in range(nr_channels): 410 | signal = raw._data[i_pick] 411 | signal = signal / np.ptp(signal) 412 | ax.plot(raw.times, signal - i_pick, color=cmap[i_pick], lw=1) 413 | 414 | ymin = -nr_channels + 0.25 415 | ymax = 0.45 416 | ax.set( 417 | xlim=(raw.times[0], raw.times[-1]), 418 | xticks=np.arange(0, 2.01, 0.5), 419 | yticks=[], 420 | ylim=(ymin, ymax), 421 | title=label, 422 | ) 423 | 424 | 425 | def plot_psd( 426 | ax, raw, SNR, peak, cmap=["#2d004f", "#254f00", "#000000"], bin_width=0 427 | ): 428 | """Convenience function for plotting PSDs and SNR for a specific band. 429 | 430 | Parameters 431 | ---------- 432 | ax : instance of Axes 433 | The axes to plot to. 434 | raw : instance of Raw 435 | Raw instance containing traces for which PSD is computed. 436 | SNR : array, 1-D 437 | array containing SNR values to be annotated, output of get_SNR. 438 | peak : 439 | Peak frequency to highlight. 440 | cmap : 441 | Colors for PSDs, specify for each channel. 442 | bin_width : 443 | Bandwidth to highlight, [peak-bin_width, peak-bin_width] 444 | """ 445 | nr_seconds = 3 446 | 447 | n_fft = int(nr_seconds * raw.info["sfreq"]) 448 | psd, freqs = mne.time_frequency.psd_welch(raw, fmin=1, fmax=70, n_fft=n_fft) 449 | 450 | for i in range(len(raw.ch_names)): 451 | ax.plot(freqs, 10 * np.log10(psd[i].T), color=cmap[i], lw=1) 452 | 453 | # plot aperiodic fit as example for first electrode 454 | fg = fooof.FOOOF() 455 | idx1 = 0 456 | fg.fit(freqs, psd[idx1]) 457 | ax.plot( 458 | fg.freqs, 10 * fg._ap_fit, color=cmap[idx1], linestyle="--", alpha=0.8 459 | ) 460 | idx_max = np.argmax(psd[idx1]) 461 | peak_freq = freqs[idx_max] 462 | peak_freq = fooof.analysis.get_band_peak_fm(fg, [peak - 2, peak + 2])[0] 463 | 464 | ax.plot( 465 | [peak_freq, peak_freq], 466 | [10 * fg._ap_fit[idx_max], 10 * np.log10(psd[idx1][idx_max])], 467 | lw=1, 468 | zorder=-3, 469 | color=cmap[idx1], 470 | ) 471 | 472 | for i in range(len(raw.ch_names)): 473 | ax.text( 474 | 45, 475 | -4.5 * i + 0.95 * ax.get_ylim()[1], 476 | "SNR=%.1f dB" % SNR[i], 477 | color=cmap[i], 478 | ha="right", 479 | ) 480 | 481 | # set axes properties 482 | ax.set( 483 | xlim=(1, 45), 484 | xlabel="frequency [Hz]", 485 | ylabel="log PSD", 486 | xticks=[10, 20, 30, 40], 487 | yticklabels=[], 488 | ) 489 | 490 | # highlight frequency band 491 | if bin_width > 0: 492 | ax.axvspan( 493 | peak - bin_width, 494 | peak + bin_width, 495 | color="gray", 496 | alpha=0.25, 497 | zorder=-3, 498 | ) 499 | 500 | 501 | def get_SNR(raw, fmin=1, fmax=55, seconds=3, freq=[8, 13]): 502 | """Compute power spectrum and calculate 1/f-corrected SNR in one band. 503 | 504 | Parameters 505 | ---------- 506 | raw : instance of Raw 507 | Raw instance containing traces for which to compute SNR 508 | fmin : float 509 | minimum frequency that is used for fitting spectral model. 510 | fmax : float 511 | maximum frequency that is used for fitting spectral model. 512 | seconds: float 513 | Window length in seconds, converts to FFT points for PSD calculation. 514 | freq : list | [8, 13] 515 | SNR in that frequency window is computed. 516 | Returns 517 | ------- 518 | SNR : array, 1-D 519 | Contains SNR (1/f-corrected, for a chosen frequency) for each channel. 520 | """ 521 | SNR = np.zeros((len(raw.ch_names),)) 522 | n_fft = int(seconds * raw.info["sfreq"]) 523 | psd, freqs = mne.time_frequency.psd_welch( 524 | raw, fmin=fmin, fmax=fmax, n_fft=n_fft 525 | ) 526 | 527 | fm = fooof.FOOOFGroup() 528 | fm.fit(freqs, psd) 529 | 530 | for pick in range(len(raw.ch_names)): 531 | psd_corr = 10 * np.log10(psd[pick]) - 10 * fm.get_fooof(pick)._ap_fit 532 | idx = np.where((freqs > freq[0]) & (freqs < freq[1]))[0] 533 | idx_max = np.argmax(psd_corr[idx]) 534 | SNR[pick] = psd_corr[idx][idx_max] 535 | 536 | return SNR 537 | --------------------------------------------------------------------------------