diff --git a/EEGToolkit.egg-info/PKG-INFO b/EEGToolkit.egg-info/PKG-INFO new file mode 100644 index 0000000..0dfecec --- /dev/null +++ b/EEGToolkit.egg-info/PKG-INFO @@ -0,0 +1,72 @@ +Metadata-Version: 2.1 +Name: EEGToolkit +Version: 2.0.0 +Summary: A package for EEG data analysis of reaction time-delay experiments. +Home-page: https://github.com/AxelGiottonini/AP-EEG.git +Author: Axel Giottonini, Noah Kleinschmidt, Kalvin Dobler +Author-email: axel.giottonini@unifr.ch, noah.kleinschmidt@students.unibe.ch, kalvin.dobler@unifr.ch +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) +Classifier: Operating System :: OS Independent +Classifier: Topic :: Scientific/Engineering :: Information Analysis +Classifier: Topic :: Scientific/Engineering :: Visualization +Requires-Python: >=3.6 +Description-Content-Type: text/markdown + +# AP-EEG + +## Project description +> ## EEG data analysis +> Students who choose this task ill be provided with the raw EEG recording of one channel, sampled at $500 [Hz]$ which was recorded from a participant presented with auditory stimuli. The students will also receive an events file describing when during the recording one out of two possible sounds were presented to the subject. +> ### Task 1 +> Extract the time-period from $0.1 [sec]$ before to $1 [sec]$ after each event for the EEG signal. Store these time-periods in two arrays, one for each of the two event-/sound-types. +> ### Task 2 +> Plot the average response for each type of event, with shading for the standard error. On the plot, also indicate $t=0$, the amplitude of the signal in microvolts (Y-axis), and the time relative to sound onset in $[ms]$ (X-axis). +> ### Task 3 +> For each sound type, for each time-point after sound onset, test wheter it is significantly different across trials from the baseline. Also test at what time-points after sound onset the EEG signals for the two sound-types are different from each other. Produce one plot containing the average EEG signal of each sound type, along with lines indicating the significant time-periods. + +## Milestones +> - **Part 1: Project management and first deliverable** (*Deadline: April 28*) +> - **Part 2: GitHub and second deliverable** (*Deadline: May 5*) +> - **Part 3: Classes and refactoring** (*Deadline: May 12*) +> - **Part 4: Unit tests and issues** (*Deadline: May 19*) +> - **Part 5: Third deliverable, presentation and virtual environment** (*Deadline: June 2*) + +## Project roadmap + +### Main Objective +The goal is to develop a small package with CLI to evaluate EEG data for reaction delays given different auditory stimuli. +While the outset data is given as a binary stimulus experiment, extending to an arbitrary number of different stimuli would be +a desirable extention. + + +### Implementation + +#### General TODOs +- [x] proper **Docstrings** and comments in general ... +- [x] re-factoring for private methods ... +- [x] requirements.txt +- [x] setup.py + - [ ] (optional but kinda cool) adding EEGData to PYTHONPATH to allow direct CLI calling... +- [ ] `testpypi` distribution +- [x] HTML documentation + +#### CLI TODOs: +- [x] Core is pretty much finished already... +- [x] Change the `argparse` settings to allow defaults for parameters like x_scale to allow easier usage + +#### Task TODOs: +##### Task 1: +- [x] Event extraction is pretty much finished already +> Possible additions:
+> - [x] split reading the files and extracting data, thereby allowing different filetypes to be processed. In case this is done, we might add support for `txt` / `tsv` / `csv` files for more general applicability of the software. + + +##### Task 2: +- [x] plotting is pretty much finished already... +- [ ] Add units to the figure axes labels +- [x] (optional) adjust color scheme ... + +##### Task 3: +- [x] What about the intra-signal baseline-comparison ? +- [x] signal-comparison is finished already... diff --git a/EEGToolkit.egg-info/SOURCES.txt b/EEGToolkit.egg-info/SOURCES.txt new file mode 100644 index 0000000..43194e0 --- /dev/null +++ b/EEGToolkit.egg-info/SOURCES.txt @@ -0,0 +1,15 @@ +README.md +setup.py +EEGToolkit/__init__.py +EEGToolkit/main.py +EEGToolkit.egg-info/PKG-INFO +EEGToolkit.egg-info/SOURCES.txt +EEGToolkit.egg-info/dependency_links.txt +EEGToolkit.egg-info/entry_points.txt +EEGToolkit.egg-info/top_level.txt +EEGToolkit/EEGData/EEGData.py +EEGToolkit/EEGData/__init__.py +EEGToolkit/EEGStats/EEGStats.py +EEGToolkit/EEGStats/__init__.py +EEGToolkit/auxiliary/__init__.py +EEGToolkit/auxiliary/auxiliary.py \ No newline at end of file diff --git a/EEGToolkit.egg-info/dependency_links.txt b/EEGToolkit.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/EEGToolkit.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/EEGToolkit.egg-info/entry_points.txt b/EEGToolkit.egg-info/entry_points.txt new file mode 100644 index 0000000..454993d --- /dev/null +++ b/EEGToolkit.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +EEGToolkit = EEGToolkit.EEGData:main diff --git a/EEGToolkit.egg-info/top_level.txt b/EEGToolkit.egg-info/top_level.txt new file mode 100644 index 0000000..1aac216 --- /dev/null +++ b/EEGToolkit.egg-info/top_level.txt @@ -0,0 +1 @@ +EEGToolkit diff --git a/EEGToolkit/EEGData.py b/EEGToolkit/EEGData/EEGData.py similarity index 91% rename from EEGToolkit/EEGData.py rename to EEGToolkit/EEGData/EEGData.py index 03bb9c5..86479b0 100644 --- a/EEGToolkit/EEGData.py +++ b/EEGToolkit/EEGData/EEGData.py @@ -5,13 +5,25 @@ describing event metadata as a 2D array, describing both the timepoints and the type of event in two columns. """ +import sys +import os -import os +import subprocess +import inspect import argparse import numpy as np import pandas as pd import matplotlib.pyplot as plt -from EEGStats import plot_signal, difference_plot + +try: + from EEGToolkit.EEGStats import plot_signal, difference_plot +except ImportError: + abspath = os.path.abspath(__file__) + dname = os.path.dirname(os.path.dirname(abspath)) + sys.path.append(dname) + + from EEGStats import plot_signal, difference_plot + supported_filetypes = [ "npy", "tsv", "csv", "txt" ] class EEGData(): @@ -156,7 +168,8 @@ def read( self, signal_path : str = None , event_path : str = None ) -> None: def extract(self, start_sec:float, stop_sec:float, - event_type : ( int or tuple or list or np.ndarray ) = None) -> np.ndarray: + event_type : ( int or tuple or list or np.ndarray ) = None, + **kwargs ) -> np.ndarray: """ Extracts data for a specific (set of) event(s) from the loaded data. And returns the data as numpy ndarrays (or a list thereof, in case of @@ -420,7 +433,8 @@ def summary(self, n = len(data) # generate a new figure - fig, ax = plt.subplots(n,n) + figsize = kwargs.pop( "figsize", ( 3*n,2*n ) ) + fig, ax = plt.subplots(n,n, figsize = figsize ) # setup a baseline reference, either with the computed # baselines or None ... @@ -439,6 +453,7 @@ def summary(self, x_scale, y_scale, baseline = baseline[i], make_legend = make_legend, + significance_level = significance_level, ax = ax[i,i] ) ax[i,i].set_title(f"Signal {signals[i]}") @@ -647,12 +662,12 @@ def main(): formatter_class=argparse.RawDescriptionHelpFormatter,description = descr1, epilog = descr2 ) parser.add_argument( "--eeg_path", "--eeg", - type=str, required=True, + type=str, help = f"A file containing EEG signal data. Supported filetypes are {supported_filetypes}" ) parser.add_argument( "--event_path", "--event", - type=str, required=True, + type=str, help = f"A file containing event metadata for the signal file. Supported filetypes are {supported_filetypes}" ) parser.add_argument( @@ -662,7 +677,7 @@ def main(): ) parser.add_argument( "--sampling_frequency", "--freq", "-f", - type=float, required=True, + type=float, help = "The frequency at which the EEG signal data was recorded (in Hertz)." ) parser.add_argument( @@ -672,12 +687,12 @@ def main(): ) parser.add_argument( "--start_sec", "--start", "-s", - type=float, required=True, + type=float, help = "The upstream time-padding for event extraction (in seconds)." ) parser.add_argument( "--stop_sec", "--stop", "-e", - type=float, required=True, + type=float, help = "The downstream time-padding for event extraction (in seconds)." ) @@ -696,23 +711,57 @@ def main(): type=float, default = 1000, help = "A scaling factor for the signal-scale (y-values) from volts to some other unit. Default is 1000 (= millivolts)." ) + + parser.add_argument( + "--viewer", "-i", + action="store_true", + default = False, + help = "Open the EEGToolKit Viewer GUI in a web browser." + ) args = parser.parse_args() - # the main program (reading datafiles, extracting, and summarizing) - data = EEGData(args.eeg_path, args.event_path, args.sampling_frequency) - data.extract( args.start_sec, args.stop_sec ) - if args.baseline: - data.baseline() - data.summary( - significance_level = args.p_value, - x_scale = args.x_scale, - y_scale = args.y_scale, - output = args.output - ) - - if args.output is not None: - print( f"Output saved successfully to: '{args.output}'" ) + # if the viewer is being called then we want to just open the + # viewer and nothing else + if args.viewer: + # first we need to get the relative location of the main. + # py file within the package. + directory = os.path.dirname( + inspect.getfile( plot_signal ) + ) + directory = os.path.dirname( directory ) + main_file = f"{directory}/main.py" + + # then we call the web interface + print( "Starting the \033[94mEEGToolKit \033[96mViewer" ) + subprocess.run( f"streamlit run {main_file}", shell = True ) + + else: + + # the main program (reading datafiles, extracting, and summarizing) + try: + data = EEGData(args.eeg_path, args.event_path, args.sampling_frequency) + data.extract( args.start_sec, args.stop_sec ) + if args.baseline: + data.baseline() + data.summary( + significance_level = args.p_value, + x_scale = args.x_scale, + y_scale = args.y_scale, + output = args.output + ) + + if args.output is not None: + print( f"Output saved successfully to: '{args.output}'" ) + except FileNotFoundError as e: + print(e) + return + except TypeError as e: + print(e) + return + except ValueError as e: + print(e) + return if __name__ == "__main__": diff --git a/EEGToolkit/EEGData/__init__.py b/EEGToolkit/EEGData/__init__.py new file mode 100644 index 0000000..d3698d1 --- /dev/null +++ b/EEGToolkit/EEGData/__init__.py @@ -0,0 +1 @@ +from .EEGData import * \ No newline at end of file diff --git a/EEGToolkit/EEGData/__main__.py b/EEGToolkit/EEGData/__main__.py new file mode 100644 index 0000000..8c231fd --- /dev/null +++ b/EEGToolkit/EEGData/__main__.py @@ -0,0 +1,173 @@ +import sys +import os + +import subprocess +import inspect +import argparse +import matplotlib.pyplot as plt + +abspath = os.path.abspath(__file__) +dname = os.path.dirname(os.path.dirname(abspath)) +sys.path.append(dname) + +from EEGStats import plot_signal +from EEGData import EEGData, supported_filetypes + + +def main(): + """ + The main function called through the CLI + + Example Usage + ------------- + python3 ./EEGToolkit/EEGData.py \ + --eeg_path "data/eeg.npy" \ + --event_path "data/events.npy" \ + --sampling_frequency 500 \ + --p_value 0.05 \ + --start_sec -0.3 \ + --stop_sec 1.0 \ + --x_scale 1000 \ + --y_scale 1000 \ + --output "./test.png" + """ + + descr1 = """ + +----------------------------------------------------- +▒█▀▀▀ ▒█▀▀▀ ▒█▀▀█ ▀▀█▀▀ █▀▀█ █▀▀█ █░░ ▒█░▄▀ ░▀░ ▀▀█▀▀ +▒█▀▀▀ ▒█▀▀▀ ▒█░▄▄ ░▒█░░ █░░█ █░░█ █░░ ▒█▀▄░ ▀█▀ ░░█░░ +▒█▄▄▄ ▒█▄▄▄ ▒█▄▄█ ░▒█░░ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▒█░▒█ ▀▀▀ ░░▀░░ +----------------------------------------------------- + +This script takes in two data files of EEG signal data and accompanying event-trigger metadata. It performs intra- and inter-signal type comparisons using pair-wise T-Tests over the time-series, highlighting significantly different stretches and producing a summary figure. + """ + descr2 = f""" + +Input Data +---------- +Accepted input file types are {supported_filetypes}. The EEG-signal datafile must specify a 1D array of measurements, while the trigger metadata file must specify a 2D array (2 columns) of trigger time points and event classifier labels (numerically encoded). + """ + + parser = argparse.ArgumentParser( prefix_chars = "-", + formatter_class=argparse.RawDescriptionHelpFormatter,description = descr1, epilog = descr2 ) + parser.add_argument( + "--eeg_path", "--eeg", + type=str, + help = f"A file containing EEG signal data. Supported filetypes are {supported_filetypes}" + ) + parser.add_argument( + "--event_path", "--event", + type=str, + help = f"A file containing event metadata for the signal file. Supported filetypes are {supported_filetypes}" + ) + parser.add_argument( + "--output", "-o", + type=str, default = None, + help = "An output file into which the output summary figure should be saved. If none is provided (default) the Figure will simply be shown." + ) + parser.add_argument( + "--sampling_frequency", "--freq", "-f", + type=float, + help = "The frequency at which the EEG signal data was recorded (in Hertz)." + ) + parser.add_argument( + "--p_value", "-p", + type=float, default = 0.05, + help = "The significance threshold at which to accept two signals being significantly different at a T-Test comparison. Default is 0.05." + ) + parser.add_argument( + "--start_sec", "--start", "-s", + type=float, + help = "The upstream time-padding for event extraction (in seconds)." + ) + parser.add_argument( + "--stop_sec", "--stop", "-e", + type=float, + help = "The downstream time-padding for event extraction (in seconds)." + ) + + parser.add_argument( + "--baseline", "-b", + type=bool, default = True, + help = "Perform baseline comparison for each event type using the same significance threshold as used for inter-signal comparisons. Will be performed by default." + ) + parser.add_argument( + "--x_scale", "-x", + type=float, default = 1000, + help = "A scaling factor for the time-scale (x-values) from seconds to some other unit. Default is 1000 (= milliseconds)." + ) + parser.add_argument( + "--y_scale", "-y", + type=float, default = 1000, + help = "A scaling factor for the signal-scale (y-values) from volts to some other unit. Default is 1000 (= millivolts)." + ) + + parser.add_argument( + "--viewer", "-i", + action="store_true", + default = False, + help = "Open the EEGToolKit Viewer GUI in a web browser." + ) + + args = parser.parse_args() + + # if the viewer is being called then we want to just open the + # viewer and nothing else + if args.viewer: + # first we need to get the relative location of the main. + # py file within the package. + directory = os.path.dirname( + inspect.getfile( plot_signal ) + ) + directory = os.path.dirname( directory ) + main_file = os.path.join(directory, "__main__.py") + #main_file = f"{directory}/__main__.py" + + # then we call the web interface + print( "Starting the \033[94mEEGToolKit \033[96mViewer" ) + subprocess.run( f"streamlit run {main_file}", shell = True ) + + else: + + # the main program (reading datafiles, extracting, and summarizing) + try: + data = EEGData(args.eeg_path, args.event_path, args.sampling_frequency) + data.extract( args.start_sec, args.stop_sec ) + if args.baseline: + data.baseline() + data.summary( + significance_level = args.p_value, + x_scale = args.x_scale, + y_scale = args.y_scale, + output = args.output + ) + + if args.output is not None: + print( f"Output saved successfully to: '{args.output}'" ) + except FileNotFoundError as e: + print(e) + return + except TypeError as e: + print(e) + return + except ValueError as e: + print(e) + return + +if __name__ == "__main__": + + test_mode = False + if not test_mode: + main() + else: + print( "Running in Test Mode" ) + + eeg = "./data/eeg.npy" + events = "./data/events.npy" + + e = EEGData( eeg, events, 500 ) + e.extract( -0.3, 1 ) + e.baseline() + e.summary( 1000, 1000, output = "./test.pdf" ) + plt.show() \ No newline at end of file diff --git a/EEGToolkit/EEGData/__pycache__/EEGData.cpython-38.pyc b/EEGToolkit/EEGData/__pycache__/EEGData.cpython-38.pyc new file mode 100644 index 0000000..081ef9a Binary files /dev/null and b/EEGToolkit/EEGData/__pycache__/EEGData.cpython-38.pyc differ diff --git a/EEGToolkit/EEGData/__pycache__/__init__.cpython-38.pyc b/EEGToolkit/EEGData/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..49aba57 Binary files /dev/null and b/EEGToolkit/EEGData/__pycache__/__init__.cpython-38.pyc differ diff --git a/EEGToolkit/EEGStats.py b/EEGToolkit/EEGStats/EEGStats.py similarity index 99% rename from EEGToolkit/EEGStats.py rename to EEGToolkit/EEGStats/EEGStats.py index bb6c53b..9cce67f 100644 --- a/EEGToolkit/EEGStats.py +++ b/EEGToolkit/EEGStats/EEGStats.py @@ -186,7 +186,8 @@ def plot_signal( x_scale, y_scale, baseline, - make_legend + make_legend, + significance_level = significance_level ) # we show the figure only if no ax was provided diff --git a/EEGToolkit/EEGStats/__init__.py b/EEGToolkit/EEGStats/__init__.py new file mode 100644 index 0000000..6ec3123 --- /dev/null +++ b/EEGToolkit/EEGStats/__init__.py @@ -0,0 +1 @@ +from .EEGStats import * \ No newline at end of file diff --git a/EEGToolkit/EEGStats/__pycache__/EEGStats.cpython-38.pyc b/EEGToolkit/EEGStats/__pycache__/EEGStats.cpython-38.pyc new file mode 100644 index 0000000..154c94f Binary files /dev/null and b/EEGToolkit/EEGStats/__pycache__/EEGStats.cpython-38.pyc differ diff --git a/EEGToolkit/EEGStats/__pycache__/__init__.cpython-38.pyc b/EEGToolkit/EEGStats/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..d8c431a Binary files /dev/null and b/EEGToolkit/EEGStats/__pycache__/__init__.cpython-38.pyc differ diff --git a/EEGToolkit/__main__.py b/EEGToolkit/__main__.py new file mode 100644 index 0000000..9589714 --- /dev/null +++ b/EEGToolkit/__main__.py @@ -0,0 +1,159 @@ +""" +This script defines an interactive +web-app for the EEGData package. +""" +import sys +import os + +import streamlit as st +import pandas as pd + +abspath = os.path.abspath(__file__) +dname = os.path.dirname(os.path.dirname(abspath)) +sys.path.append(dname) + +from EEGData import supported_filetypes +from auxiliary import Session, stEEGData + +if __name__ == "__main__": + session = Session() + + # ================================================================= + # Header Section + # ================================================================= + + + st.markdown( + """ + # EEGToolKit Viewer + """, + ) + + # ================================================================= + # Data Input Section + # ================================================================= + + file_container = st.container() + signal_col, events_col = file_container.columns( 2 ) + + # add a signal datafile + signal_file = signal_col.file_uploader( + "EEG Signal Data", + help = f"Upload here a datafile specifying a 1D array of EEG signal data.", + type = supported_filetypes + ) + if signal_file is not None: + session.add( "signal_filename", signal_file.name ) + session.add( "signal_file", signal_file, export = False ) + + # add an events metadata file + events_file = events_col.file_uploader( + "Events Metadata", + help = f"Upload here a datafile specifying a 2-column array of events metadata for the signal data file.", + type = supported_filetypes + ) + if events_file is not None: + session.add( "events_filename", events_file.name ) + session.add( "events_file", events_file, export = False ) + + + # ================================================================= + # Data Processing Section + # ================================================================= + + # In case anybody is wondering why the weird column setup here. The answer + # is that streamlit does not (yet) support nested columns nor stretching objects + # over multiple columns. Hence, one column, one object, if they should "appear" + # in side by side, one above the other etc. layouts we need to have multiple + # column blocks... + + controls = st.container() + upper_ctrl_col1, upper_ctrl_col2 = controls.columns( (1, 3) ) + mid_ctrl_col1, mid_ctrl_col2, mid_ctrl_col3 = controls.columns( (2, 3, 3 ) ) + lower_ctrl_col1, lower_ctrl_col2, lower_ctrl_col3 = controls.columns( (2, 3, 3 ) ) + + compute_baseline = upper_ctrl_col2.checkbox( + "Compare Baseline", + help = "Compute the baseline of a given signal type / event and compares the signal to the baseline through positition-wise T-Tests.", + value = True + ) + + significance_level = upper_ctrl_col2.number_input( + "Significance Level", + help = "Choose a significance threshold level for signal-signal and signal-baseline comparisons", + min_value = 1e-5, max_value = 1.0, value = 0.05, step = 1e-3, format = "%e" + ) + session.add( "significance_level", significance_level ) + + frequency = upper_ctrl_col2.number_input( + "Sampling Frequency", + help = "The sampling frequency in `Hertz` at which the signal was recorded", + min_value = 1, max_value = 100000, value = 500, step = 100, format = "%d", + ) + session.add( "frequency", frequency ) + + + start_sec = mid_ctrl_col2.number_input( + "Upstream Buffer", + help = "The time buffer pre-event to extract (in seconds).", + min_value = 1e-4, max_value = 10.0, value = 0.01, step = 0.001, format = "%e", + ) + session.add( "upstream_buffer", start_sec ) + + stop_sec = mid_ctrl_col3.number_input( + "Downstream Buffer", + help = "The time buffer post-event to extract (in seconds).", + min_value = 1e-4, max_value = 10.0, value = 1.0, step = 0.001, format = "%e", + ) + session.add( "downstream_buffer", stop_sec ) + + xscale = lower_ctrl_col2.number_input( + "Timeframe Scale", + help = "A scaling factor to adjust x-axis time scale. E.g. `1000` to adjust data from seconds to milliseconds.", + min_value = 1, max_value = 100000, value = 1000, step = 100, format = "%d", + ) + session.add( "timescale", xscale ) + + yscale = lower_ctrl_col3.number_input( + "Signal Scale", + help = "A scaling factor to adjust y-axis signal scale. E.g. `1000` to adjust from volts to millivolts.", + min_value = 1, max_value = 100000, value = 1000, step = 100, format = "%d", + ) + session.add( "signalscale", yscale ) + + # slightly ugly splitting of the text, but the only way to adjust the text to the column layout I want... + mid_ctrl_col1.markdown( "Once your setup is done, press the `Compute` button below to " ) + lower_ctrl_col1.markdown( "commence data evaluation." ) + run_button = lower_ctrl_col1.button( + "Compute", + help = "Perform computations and output a figure", + ) + + # ================================================================= + # Computational Section + # ================================================================= + + figure_container = st.container() + if run_button: + if signal_file is None: + st.error( "No Signal File was provided so far!" ) + st.stop() + if events_file is None: + st.error( "No Events File was provided so far!" ) + st.stop() + eegdata = stEEGData( + signal_path = signal_file, + event_path = events_file, + sampling_frequency = frequency + ) + eegdata.extract( -start_sec, stop_sec ) + + if compute_baseline: + eegdata.baseline() + fig = eegdata.summary( + x_scale = xscale, + y_scale = yscale, + significance_level = significance_level + ) + + figure_container.pyplot( fig ) \ No newline at end of file diff --git a/EEGToolkit/auxiliary/__init__.py b/EEGToolkit/auxiliary/__init__.py new file mode 100644 index 0000000..ea86998 --- /dev/null +++ b/EEGToolkit/auxiliary/__init__.py @@ -0,0 +1 @@ +from .auxiliary import * \ No newline at end of file diff --git a/EEGToolkit/auxiliary/__pycache__/__init__.cpython-38.pyc b/EEGToolkit/auxiliary/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..a772f43 Binary files /dev/null and b/EEGToolkit/auxiliary/__pycache__/__init__.cpython-38.pyc differ diff --git a/EEGToolkit/auxiliary/__pycache__/auxiliary.cpython-38.pyc b/EEGToolkit/auxiliary/__pycache__/auxiliary.cpython-38.pyc new file mode 100644 index 0000000..79ac932 Binary files /dev/null and b/EEGToolkit/auxiliary/__pycache__/auxiliary.cpython-38.pyc differ diff --git a/EEGToolkit/auxiliary/auxiliary.py b/EEGToolkit/auxiliary/auxiliary.py new file mode 100644 index 0000000..1e80f37 --- /dev/null +++ b/EEGToolkit/auxiliary/auxiliary.py @@ -0,0 +1,167 @@ +""" +Auxiliary functions to work within the streamlit environment +""" +import sys +import os + +import streamlit as st +from copy import deepcopy + +try: + from EEGToolkit.EEGData import EEGData, supported_filetypes +except ImportError: + abspath = os.path.abspath(__file__) + dname = os.path.dirname(os.path.dirname(abspath)) + sys.path.append(dname) + + from EEGData import EEGData, supported_filetypes + +class Session: + """ + A class to handle storing values into and getting them from the streamlit session state. + """ + def __init__(self): + + # a list of keys to include in the + # export session dictionary + self._to_export = set() + + def add( self, key, value, export = True ): + """ + Add a new item to the session but + can also change an existing item. + + Parameters + ---------- + key : str + The key of the new item + value + The value to store of the new item + export : bool + Wether or not to include the item in the exported session dictioanry. + """ + st.session_state[ key ] = value + + if export: + self._to_export.add( key ) + + def set( self, key, value, export = True ): + """ + A synonym of `add` + """ + self.add( key, value, export ) + + def remove( self, key ): + """ + Remove an item from the session + """ + try: + st.session_state.pop( key ) + self._to_export.remove( key ) + except: + pass + + def get( self, key, default = None, rm = False, copy = False ): + """ + Tries to get an item from the session + and returns the default if this fails. + If rm = True is set, the item will be + removed after getting. + If copy = True is set, it will return a deepcopy. + """ + try: + value = st.session_state[ key ] + if copy: + value = deepcopy(value) + except: + value = default + + if rm: + self.remove( key ) + + return value + + def setup( self, key, value ): + """ + Stores an item to the session only if it's not already present. + """ + if not self.exists( key ): + self.add( key, value ) + + def exists( self, key ): + """ + Checks if an item is in the session. Returns True if so. + """ + verdict = key in st.session_state + return verdict + + def hasvalue( self, key ): + """ + Checks if an item is in the session and is also not None. + Returns True if both conditions are fulfilled. + """ + exists = self.exists( key ) + not_none = self.get( key, None ) is not None + verdict = exists and not_none + return verdict + + def export( self ): + """ + Exports a copy of the session state for better readablity wherein non-intelligible entries are removed. + """ + to_export = st.session_state[ self._to_export ] + return to_export + + + +class stEEGData( EEGData ): + """ + A streamlit compatible version of EEGData + The primary difference here is that it replaces the original filepath variables + with filepath.name arguments to work with the string and not the UploadedFile objects. + """ + def __init__( self, *args, **kwargs ): + super().__init__(*args, **kwargs) + + def _filesuffix(self, filepath): + """ + Returns the suffix from a filepath + """ + suffix = os.path.basename( filepath.name ) + suffix = suffix.split(".")[-1] + return suffix + + def _csv_delimiter(self, filepath): + """ + Checks if a csv file is , or ; delimited and returns the + correct delmiter to use... + """ + # open the file and read + content = deepcopy(filepath).read().decode() + + # check if a semicolon is present + # if so, we delimit at ; + has_semicolon = ";" in content + delimiter = ";" if has_semicolon else "," + + return delimiter + + def _check_sanity(self, signal_path, event_path, sampling_frequency): + """ + Checks if valid data inputs were provided + """ + + # check if the datafiles conform to suppored filetypes + fname = os.path.basename(signal_path.name) + if not any( [ fname.endswith(suffix) for suffix in supported_filetypes ] ): + suffix = fname.split(".")[-1] + raise TypeError( f"The signal datafile could not be interpreted ('.{suffix}'), only {supported_filetypes} files are supported!" ) + + fname = os.path.basename(event_path.name) + if not any( [ fname.endswith(suffix) for suffix in supported_filetypes ] ): + suffix = fname.split(".")[-1] + raise TypeError( f"The event datafile could not be interpreted ('.{suffix}'), only {supported_filetypes} files are supported!" ) + + # check if the frequency is a positive number + if not sampling_frequency > 0: + raise ValueError( f"Sampling frequency {sampling_frequency} must be a positive value" ) \ No newline at end of file diff --git a/data/events_extended.tsv b/data/events_extended.tsv new file mode 100644 index 0000000..7a65643 --- /dev/null +++ b/data/events_extended.tsv @@ -0,0 +1,4320 @@ +40759 0 +41903 0 +42979 1 +44089 1 +45209 1 +46231 0 +47246 1 +48381 0 +52148 0 +53245 1 +54360 0 +55364 1 +56444 1 +57589 1 +58601 0 +63149 1 +64217 1 +65231 0 +66272 0 +67348 0 +68471 1 +69652 1 +73740 0 +74880 0 +75948 0 +77005 1 +78116 1 +79305 0 +80481 0 +81558 1 +85079 0 +86153 1 +87211 0 +88411 1 +89505 1 +90664 1 +91762 1 +95635 0 +96809 0 +97836 0 +98911 0 +99983 1 +101155 1 +102214 1 +105955 0 +107111 1 +108113 0 +109195 0 +110295 1 +111355 0 +112434 1 +113504 0 +117990 1 +119029 1 +120137 1 +121222 0 +122387 0 +123486 0 +124665 1 +125692 0 +129126 1 +130310 0 +131341 1 +132412 0 +133587 1 +134750 0 +135805 1 +139662 1 +140751 0 +141879 0 +142914 0 +144013 0 +145104 1 +146262 0 +147429 1 +151092 1 +152184 1 +153310 0 +154444 0 +155615 0 +156653 1 +157721 1 +161796 1 +162848 0 +163952 1 +165114 0 +166231 1 +167291 0 +168381 1 +169417 0 +173628 0 +174674 0 +175859 0 +177036 0 +178125 1 +179266 1 +180371 1 +181404 0 +185094 1 +186112 1 +187183 1 +188305 1 +189335 1 +190410 0 +191560 0 +192615 0 +196720 0 +197780 1 +198878 1 +199963 0 +201106 1 +202117 1 +203186 0 +206908 1 +208035 0 +209192 1 +210289 0 +211388 0 +212411 1 +213589 0 +218038 0 +219130 0 +220245 1 +221288 1 +222483 0 +223487 0 +224549 0 +225698 1 +230629 0 +231663 1 +232752 0 +233793 0 +234966 0 +236101 1 +237180 1 +241433 1 +242612 1 +243777 0 +244912 1 +246037 0 +247229 0 +248389 1 +252343 1 +253368 0 +254424 1 +255505 1 +256587 0 +257613 1 +258672 1 +259802 0 +264310 1 +265327 0 +266521 1 +267565 0 +268687 0 +269700 0 +270712 1 +271861 0 +276340 1 +277435 1 +278444 1 +279583 1 +280713 1 +281833 0 +283001 0 +287510 0 +288536 0 +289569 1 +290626 1 +291800 1 +292997 0 +294047 0 +297820 0 +298990 0 +300026 1 +301221 0 +302319 1 +303357 1 +304509 1 +305556 0 +309506 1 +310698 0 +311758 1 +312889 0 +314069 0 +315081 1 +316101 0 +320683 1 +321850 1 +322920 1 +324015 0 +325165 1 +326211 1 +327334 0 +328503 0 +332409 1 +333569 0 +334705 1 +335787 0 +336961 0 +338118 1 +339227 0 +340293 0 +344329 0 +345377 1 +346395 0 +347397 1 +348430 0 +349466 1 +350485 1 +354348 1 +355438 0 +356520 0 +357587 1 +358663 0 +359674 1 +360765 1 +361924 0 +365880 1 +366937 1 +368064 0 +369177 1 +370328 1 +371516 0 +372648 1 +376503 0 +377562 0 +378697 1 +379833 1 +380873 0 +381932 0 +383020 0 +386705 0 +387775 1 +388890 0 +389917 1 +390973 0 +392170 1 +393183 1 +394241 0 +398880 0 +399928 0 +401060 1 +402163 1 +403291 1 +404318 1 +405405 0 +406480 0 +410809 1 +411976 0 +413172 0 +414370 1 +415482 1 +416664 1 +417729 0 +418881 0 +422962 0 +424044 1 +425212 1 +426387 0 +427579 1 +428714 1 +429847 0 +434120 0 +435134 0 +436275 1 +437464 1 +438532 1 +439570 0 +440741 0 +445172 0 +446227 1 +447300 0 +448308 1 +449463 0 +450506 1 +451702 1 +455483 1 +456547 0 +457580 0 +458632 1 +459791 0 +460793 0 +461883 1 +462947 1 +467055 1 +468236 0 +469284 1 +470462 0 +471522 1 +472695 0 +473768 1 +474871 0 +478995 1 +480109 1 +481235 1 +482276 0 +483436 0 +484571 0 +485631 0 +490190 1 +491336 0 +492365 0 +493536 0 +494726 0 +495775 1 +496915 1 +497980 1 +502476 0 +503637 0 +504711 0 +505843 1 +506955 0 +507997 1 +509050 1 +513770 1 +514818 1 +515971 0 +517151 1 +518269 0 +519418 0 +520471 1 +524054 0 +525121 0 +526307 0 +527419 1 +528459 1 +529518 1 +530699 1 +531881 0 +535552 0 +536615 1 +537655 1 +538751 0 +539906 0 +540971 1 +542117 0 +546543 0 +547643 0 +548651 1 +549707 0 +550787 1 +551876 0 +552911 1 +557991 1 +559173 1 +560253 0 +561357 1 +562407 0 +563447 1 +564494 0 +565511 0 +569931 1 +571100 0 +572227 1 +573334 1 +574441 1 +575538 0 +576694 0 +577788 1 +581469 1 +582512 0 +583576 1 +584593 1 +585707 0 +586787 0 +587913 0 +591882 1 +592998 1 +594180 1 +595273 0 +596307 0 +597391 1 +598487 0 +599556 0 +603402 1 +604495 1 +605565 1 +606608 0 +607784 1 +608946 0 +610101 1 +614242 0 +615319 0 +616495 1 +617627 0 +618692 1 +619824 1 +620923 0 +622077 0 +625712 0 +626755 1 +627943 1 +628976 1 +630093 0 +631115 0 +632194 1 +633269 1 +636989 1 +637993 1 +639035 0 +640216 1 +641314 1 +642461 0 +643545 0 +648028 0 +649063 1 +650179 0 +651364 1 +652449 0 +653617 1 +654779 0 +659006 0 +660123 0 +661274 0 +662347 1 +663447 1 +664575 1 +665595 0 +666767 0 +670592 0 +671650 1 +672653 1 +673738 0 +674772 0 +675879 0 +677048 1 +678171 1 +681869 1 +682963 1 +683970 0 +685084 0 +686135 1 +687228 0 +688419 0 +692033 0 +693072 0 +694247 1 +695303 1 +696433 0 +697551 0 +698703 1 +699823 0 +704375 0 +705529 1 +706637 1 +707682 0 +708801 1 +709805 1 +710883 1 +714608 1 +715672 1 +716861 0 +718033 0 +719086 1 +720221 0 +721270 0 +725512 0 +726525 1 +727693 0 +728755 1 +729826 1 +731004 1 +732111 0 +735821 0 +736896 1 +737991 0 +739049 0 +740228 1 +741338 1 +742417 1 +743612 1 +748334 1 +749481 1 +750580 0 +751631 0 +752789 1 +753954 0 +755145 0 +756236 0 +780615 1 +781666 1 +782742 0 +783904 0 +785013 0 +786040 0 +787096 1 +788187 1 +792335 0 +793385 1 +794385 1 +795396 0 +796433 0 +797452 1 +798475 1 +802763 1 +803770 1 +804812 0 +805970 1 +807136 0 +808184 0 +809241 0 +813648 1 +814744 0 +815823 0 +816982 1 +818053 1 +819239 1 +820305 0 +821357 0 +825178 0 +826186 1 +827243 1 +828376 1 +829444 0 +830528 1 +831584 0 +835661 0 +836729 0 +837877 1 +839001 0 +840033 1 +841075 1 +842271 1 +843383 0 +847592 0 +848668 0 +849683 0 +850691 1 +851723 1 +852776 1 +853882 1 +857662 0 +858821 0 +859922 0 +861050 1 +862113 1 +863200 0 +864382 1 +865518 0 +869357 1 +870521 0 +871641 0 +872678 0 +873797 1 +874804 1 +875909 0 +877042 0 +880822 0 +881985 1 +883129 1 +884172 0 +885224 1 +886318 1 +887460 0 +891240 0 +892297 1 +893432 1 +894571 1 +895760 0 +896769 0 +897823 1 +902267 1 +903373 1 +904538 0 +905656 0 +906708 0 +907799 1 +908896 1 +910027 0 +913822 1 +914916 1 +915992 1 +917177 1 +918188 0 +919328 0 +920372 0 +924769 1 +925803 1 +926892 0 +927988 0 +929091 1 +930102 0 +931192 1 +932222 0 +936485 0 +937638 0 +938815 0 +940001 1 +941127 1 +942180 0 +943295 1 +944446 1 +948593 0 +949785 1 +950908 1 +952070 0 +953099 0 +954174 1 +955263 0 +959097 1 +960156 0 +961222 0 +962419 1 +963439 0 +964440 0 +965516 1 +969220 1 +970391 1 +971502 1 +972563 0 +973598 0 +974781 0 +975904 1 +977014 1 +980716 0 +981783 0 +982867 1 +984006 1 +985042 0 +986102 0 +987272 0 +988411 1 +992396 1 +993476 0 +994523 1 +995623 0 +996764 1 +997921 0 +999019 1 +1003100 0 +1004299 0 +1005364 1 +1006555 1 +1007642 1 +1008667 0 +1009712 1 +1010733 1 +1014451 1 +1015600 0 +1016729 0 +1017818 1 +1018882 0 +1020044 1 +1021149 1 +1022172 0 +1026317 1 +1027365 1 +1028378 0 +1029396 0 +1030420 0 +1031494 0 +1032685 0 +1036698 1 +1037877 0 +1038915 0 +1039946 1 +1041079 1 +1042221 1 +1043335 0 +1047931 1 +1048977 0 +1050122 0 +1051289 1 +1052398 0 +1053590 0 +1054603 1 +1055671 0 +1060208 1 +1061313 1 +1062437 1 +1063447 0 +1064565 1 +1065698 1 +1066798 0 +1071341 1 +1072482 0 +1073495 1 +1074641 0 +1075657 0 +1076745 0 +1077811 1 +1081501 0 +1082553 0 +1083558 1 +1084746 1 +1085801 1 +1086890 0 +1087958 1 +1089066 0 +1092875 1 +1093906 0 +1094985 1 +1096103 0 +1097240 1 +1098309 0 +1099492 1 +1100673 1 +1104513 1 +1105575 0 +1106600 0 +1107712 1 +1108836 0 +1109929 1 +1110980 0 +1116029 0 +1117067 0 +1118160 0 +1119304 1 +1120464 0 +1121649 0 +1122769 1 +1123814 1 +1127982 1 +1129080 0 +1130098 1 +1131143 1 +1132143 0 +1133146 1 +1134327 0 +1139244 0 +1140440 1 +1141560 1 +1142632 0 +1143739 1 +1144887 1 +1146016 0 +1149611 0 +1150749 1 +1151798 0 +1152924 0 +1154011 0 +1155106 1 +1156151 1 +1157212 1 +1160791 1 +1161988 1 +1163075 0 +1164127 0 +1165209 1 +1166304 1 +1167426 0 +1171136 1 +1172309 0 +1173467 0 +1174598 1 +1175776 0 +1176896 0 +1177942 0 +1179027 1 +1183233 1 +1184343 0 +1185519 0 +1186620 1 +1187701 1 +1188718 1 +1189738 0 +1193513 0 +1194552 1 +1195728 1 +1196839 1 +1197909 0 +1198939 0 +1200138 0 +1201210 0 +1204914 0 +1205935 0 +1207050 1 +1208112 1 +1209248 1 +1210326 0 +1211511 1 +1212588 1 +1217014 1 +1218167 0 +1219202 0 +1220276 1 +1221405 1 +1222497 0 +1223606 0 +1228398 0 +1229515 1 +1230555 1 +1231596 0 +1232748 0 +1233775 1 +1234949 0 +1238694 0 +1239772 1 +1240813 1 +1241859 0 +1242927 1 +1243959 1 +1245003 1 +1246106 0 +1249968 1 +1251100 0 +1252276 1 +1253367 1 +1254483 0 +1255590 0 +1256664 1 +1257684 1 +1262431 0 +1263528 1 +1264661 1 +1265821 0 +1267009 0 +1268089 0 +1269212 0 +1273013 1 +1274124 1 +1275304 1 +1276442 0 +1277636 0 +1278673 1 +1279838 0 +1280993 0 +1285105 1 +1286125 1 +1287247 1 +1288372 1 +1289402 0 +1290591 0 +1291592 0 +1295219 0 +1296347 0 +1297510 1 +1298598 1 +1299757 0 +1300766 1 +1301875 0 +1305703 1 +1306714 0 +1307855 0 +1309050 1 +1310185 0 +1311276 1 +1312322 0 +1313374 1 +1317969 1 +1319166 0 +1320339 0 +1321412 0 +1322585 0 +1323663 0 +1324740 1 +1329828 1 +1331021 1 +1332033 0 +1333087 1 +1334206 0 +1335210 0 +1336318 1 +1340951 0 +1342047 0 +1343240 1 +1344331 0 +1345464 1 +1346580 1 +1347666 1 +1348853 0 +1352542 0 +1353585 1 +1354607 1 +1355627 1 +1356821 0 +1357920 1 +1359093 1 +1360247 0 +1364982 0 +1366079 0 +1367191 1 +1368309 0 +1369410 0 +1370615 0 +1371687 1 +1376331 0 +1377358 1 +1378416 1 +1379428 1 +1380599 1 +1381774 0 +1382942 1 +1387490 1 +1388503 0 +1389636 0 +1390724 1 +1391754 1 +1392935 1 +1394057 0 +1395098 0 +1398855 0 +1399974 1 +1401049 1 +1402107 0 +1403192 1 +1404376 0 +1405503 1 +1406577 0 +1410597 1 +1411662 0 +1412802 1 +1413861 0 +1414895 1 +1415897 0 +1416930 1 +1418002 1 +1422056 1 +1423136 1 +1424138 0 +1425206 0 +1426406 1 +1427578 0 +1428687 1 +1432350 1 +1433464 0 +1434598 0 +1435758 1 +1436912 0 +1438057 0 +1439104 1 +1443441 1 +1444457 0 +1445492 1 +1446578 0 +1447675 1 +1448762 0 +1449783 0 +1450825 0 +1454440 0 +1455506 1 +1456675 0 +1457859 1 +1459048 0 +1460167 1 +1461348 1 +1465237 0 +1466320 1 +1467470 0 +1468577 0 +1469710 1 +1470774 1 +1471958 1 +1473030 1 +1477489 1 +1478547 0 +1479563 0 +1480655 1 +1481699 0 +1482758 0 +1483833 1 +1487672 0 +1488692 0 +1489789 1 +1490874 1 +1492061 0 +1493207 0 +1494278 0 +1495341 1 +1504231 1 +1505411 0 +1506542 1 +1507565 0 +1508707 1 +1509794 1 +1510833 0 +1515496 1 +1516648 0 +1517730 0 +1518837 1 +1519956 1 +1521029 0 +1522121 0 +1523276 0 +1527115 0 +1528188 1 +1529219 1 +1530283 0 +1531411 0 +1532614 1 +1533746 1 +1534813 0 +1538494 0 +1539613 0 +1540660 1 +1541851 1 +1543037 1 +1544132 0 +1545321 1 +1549231 1 +1550297 0 +1551309 0 +1552446 1 +1553616 0 +1554815 1 +1555887 1 +1559459 1 +1560528 0 +1561549 1 +1562733 1 +1563884 0 +1564976 1 +1566079 0 +1569857 0 +1570947 1 +1572040 1 +1573128 1 +1574249 1 +1575336 0 +1576359 0 +1577369 0 +1580936 1 +1582093 0 +1583114 0 +1584210 1 +1585282 0 +1586384 0 +1587420 0 +1588497 1 +1592553 0 +1593669 1 +1594828 0 +1595993 0 +1597132 1 +1598302 1 +1599451 1 +1603163 1 +1604292 0 +1605382 0 +1606408 1 +1607463 0 +1608533 1 +1609628 0 +1613882 1 +1614897 0 +1615906 0 +1617002 1 +1618048 1 +1619117 0 +1620141 0 +1621178 1 +1625341 1 +1626409 0 +1627471 1 +1628497 1 +1629523 1 +1630555 0 +1631641 0 +1632661 0 +1636949 0 +1638057 0 +1639211 1 +1640407 0 +1641411 1 +1642550 1 +1643726 0 +1647529 0 +1648540 1 +1649709 0 +1650834 0 +1651866 1 +1652965 1 +1654006 1 +1655168 0 +1659242 0 +1660284 0 +1661351 1 +1662406 1 +1663506 0 +1664550 1 +1665550 1 +1669187 1 +1670228 0 +1671260 0 +1672281 1 +1673421 0 +1674540 0 +1675634 1 +1676775 1 +1681120 0 +1682151 0 +1683159 0 +1684254 1 +1685391 1 +1686504 1 +1687584 0 +1688592 0 +1692988 1 +1694027 0 +1695100 1 +1696284 0 +1697385 0 +1698389 1 +1699479 0 +1703511 0 +1704582 1 +1705694 0 +1706704 1 +1707851 0 +1709006 1 +1710181 1 +1711344 1 +1715229 0 +1716265 0 +1717379 1 +1718387 1 +1719439 0 +1720592 1 +1721738 1 +1725521 0 +1726650 0 +1727680 1 +1728731 0 +1729833 1 +1730921 1 +1732112 1 +1736712 1 +1737763 0 +1738801 0 +1739884 0 +1740901 0 +1741905 1 +1742970 1 +1744114 0 +1747980 1 +1749040 0 +1750145 1 +1751158 1 +1752210 0 +1753348 0 +1754454 0 +1758239 1 +1759261 1 +1760349 0 +1761408 0 +1762507 0 +1763699 1 +1764760 1 +1765895 1 +1769765 1 +1770794 0 +1771918 1 +1772982 1 +1774105 0 +1775256 1 +1776332 1 +1780333 0 +1781523 1 +1782676 1 +1783711 0 +1784762 0 +1785776 0 +1786974 0 +1788015 1 +1792797 0 +1793817 1 +1794927 1 +1795979 0 +1797138 0 +1798195 0 +1799271 1 +1800447 1 +1804362 1 +1805487 0 +1806627 0 +1807734 1 +1808863 0 +1810030 1 +1811147 0 +1815673 1 +1816768 0 +1817887 1 +1819000 1 +1820070 0 +1821080 1 +1822266 0 +1823369 0 +1827477 1 +1828590 0 +1829667 0 +1830742 1 +1831894 0 +1833075 1 +1834185 1 +1837981 0 +1839072 0 +1840097 0 +1841249 1 +1842261 1 +1843348 0 +1844542 0 +1848955 1 +1850085 1 +1851180 1 +1852310 1 +1853416 0 +1854593 0 +1855649 0 +1856745 1 +1860997 1 +1862158 0 +1863171 1 +1864303 0 +1865327 1 +1866477 0 +1867650 0 +1872104 1 +1873252 0 +1874284 1 +1875322 1 +1876520 1 +1877677 0 +1878815 0 +1879862 0 +1884280 0 +1885380 1 +1886515 0 +1887522 1 +1888614 1 +1889712 0 +1890892 1 +1892052 1 +1895771 1 +1896968 1 +1898102 0 +1899245 1 +1900422 0 +1901524 0 +1902683 0 +1906916 1 +1908026 0 +1909197 1 +1910270 1 +1911322 0 +1912409 0 +1913559 1 +1914755 1 +1918311 1 +1919427 0 +1920471 0 +1921580 0 +1922743 1 +1923860 0 +1924934 1 +1926062 1 +1930454 1 +1931607 1 +1932652 0 +1933658 0 +1934779 0 +1935873 1 +1936919 0 +1941002 0 +1942192 0 +1943193 1 +1944276 1 +1945304 0 +1946444 0 +1947637 1 +1951356 0 +1952551 0 +1953682 1 +1954716 1 +1955720 1 +1956898 1 +1958028 0 +1959106 0 +1963806 1 +1964875 1 +1965920 0 +1967106 1 +1968139 0 +1969296 0 +1970441 1 +1974001 0 +1975105 1 +1976158 0 +1977269 0 +1978388 1 +1979397 0 +1980512 1 +1981689 1 +1986207 1 +1987215 1 +1988403 1 +1989472 0 +1990517 0 +1991517 0 +1992687 0 +1996578 1 +1997751 0 +1998914 1 +1999947 0 +2000972 0 +2002150 1 +2003335 1 +2004472 1 +2008379 1 +2009455 1 +2010504 0 +2011650 1 +2012663 0 +2013758 0 +2014901 0 +2018547 0 +2019724 1 +2020859 0 +2021985 1 +2023157 0 +2024342 1 +2025481 0 +2029142 0 +2030338 1 +2031389 0 +2032455 0 +2033496 1 +2034665 1 +2035853 1 +2036857 0 +2041484 0 +2042668 1 +2043745 0 +2044795 1 +2045971 1 +2046998 0 +2048039 0 +2051921 1 +2053094 1 +2054233 1 +2055353 1 +2056464 0 +2057572 1 +2058755 0 +2059911 0 +2064430 0 +2065490 0 +2066499 1 +2067564 1 +2068710 1 +2069769 0 +2070824 0 +2071936 0 +2076572 0 +2077733 0 +2078748 1 +2079924 0 +2080945 1 +2082026 1 +2083166 1 +2086738 1 +2087841 1 +2088990 1 +2090184 0 +2091335 1 +2092518 0 +2093711 0 +2094739 0 +2099365 0 +2100478 1 +2101562 1 +2102565 1 +2103652 0 +2104755 0 +2105915 0 +2107055 0 +2111319 0 +2112332 1 +2113464 0 +2114517 1 +2115612 0 +2116732 0 +2117759 1 +2122229 1 +2123417 1 +2124450 1 +2125595 0 +2126673 0 +2127798 1 +2128812 1 +2132445 0 +2133483 0 +2134680 1 +2135814 0 +2136878 0 +2138080 0 +2139135 1 +2140266 1 +2143949 0 +2145050 1 +2146081 0 +2147147 0 +2148325 1 +2149505 1 +2150604 0 +2154266 1 +2155326 0 +2156442 1 +2157625 0 +2158724 1 +2159732 0 +2160761 1 +2165362 0 +2166505 0 +2167701 1 +2168829 1 +2169836 0 +2171001 1 +2172083 1 +2173190 1 +2177595 1 +2178711 0 +2179836 1 +2180953 0 +2182150 1 +2183201 1 +2184226 1 +2185310 0 +2189669 1 +2190792 0 +2191847 0 +2192936 0 +2193994 1 +2195087 1 +2196159 1 +2200409 0 +2201552 0 +2202649 1 +2203656 1 +2204828 0 +2205865 1 +2207064 0 +2211341 0 +2212534 0 +2213704 1 +2214715 0 +2215779 0 +2216971 0 +2218058 1 +2219209 1 +40759 3 +41903 3 +42979 2 +44089 2 +45209 2 +46231 3 +47246 2 +48381 3 +52148 3 +53245 2 +54360 3 +55364 2 +56444 2 +57589 2 +58601 3 +63149 2 +64217 2 +65231 3 +66272 3 +67348 3 +68471 2 +69652 2 +73740 3 +74880 3 +75948 3 +77005 2 +78116 2 +79305 3 +80481 3 +81558 2 +85079 3 +86153 2 +87211 3 +88411 2 +89505 2 +90664 2 +91762 2 +95635 3 +96809 3 +97836 3 +98911 3 +99983 2 +101155 2 +102214 2 +105955 3 +107111 2 +108113 3 +109195 3 +110295 2 +111355 3 +112434 2 +113504 3 +117990 2 +119029 2 +120137 2 +121222 3 +122387 3 +123486 3 +124665 2 +125692 3 +129126 2 +130310 3 +131341 2 +132412 3 +133587 2 +134750 3 +135805 2 +139662 2 +140751 3 +141879 3 +142914 3 +144013 3 +145104 2 +146262 3 +147429 2 +151092 2 +152184 2 +153310 3 +154444 3 +155615 3 +156653 2 +157721 2 +161796 2 +162848 3 +163952 2 +165114 3 +166231 2 +167291 3 +168381 2 +169417 3 +173628 3 +174674 3 +175859 3 +177036 3 +178125 2 +179266 2 +180371 2 +181404 3 +185094 2 +186112 2 +187183 2 +188305 2 +189335 2 +190410 3 +191560 3 +192615 3 +196720 3 +197780 2 +198878 2 +199963 3 +201106 2 +202117 2 +203186 3 +206908 2 +208035 3 +209192 2 +210289 3 +211388 3 +212411 2 +213589 3 +218038 3 +219130 3 +220245 2 +221288 2 +222483 3 +223487 3 +224549 3 +225698 2 +230629 3 +231663 2 +232752 3 +233793 3 +234966 3 +236101 2 +237180 2 +241433 2 +242612 2 +243777 3 +244912 2 +246037 3 +247229 3 +248389 2 +252343 2 +253368 3 +254424 2 +255505 2 +256587 3 +257613 2 +258672 2 +259802 3 +264310 2 +265327 3 +266521 2 +267565 3 +268687 3 +269700 3 +270712 2 +271861 3 +276340 2 +277435 2 +278444 2 +279583 2 +280713 2 +281833 3 +283001 3 +287510 3 +288536 3 +289569 2 +290626 2 +291800 2 +292997 3 +294047 3 +297820 3 +298990 3 +300026 2 +301221 3 +302319 2 +303357 2 +304509 2 +305556 3 +309506 2 +310698 3 +311758 2 +312889 3 +314069 3 +315081 2 +316101 3 +320683 2 +321850 2 +322920 2 +324015 3 +325165 2 +326211 2 +327334 3 +328503 3 +332409 2 +333569 3 +334705 2 +335787 3 +336961 3 +338118 2 +339227 3 +340293 3 +344329 3 +345377 2 +346395 3 +347397 2 +348430 3 +349466 2 +350485 2 +354348 2 +355438 3 +356520 3 +357587 2 +358663 3 +359674 2 +360765 2 +361924 3 +365880 2 +366937 2 +368064 3 +369177 2 +370328 2 +371516 3 +372648 2 +376503 3 +377562 3 +378697 2 +379833 2 +380873 3 +381932 3 +383020 3 +386705 3 +387775 2 +388890 3 +389917 2 +390973 3 +392170 2 +393183 2 +394241 3 +398880 3 +399928 3 +401060 2 +402163 2 +403291 2 +404318 2 +405405 3 +406480 3 +410809 2 +411976 3 +413172 3 +414370 2 +415482 2 +416664 2 +417729 3 +418881 3 +422962 3 +424044 2 +425212 2 +426387 3 +427579 2 +428714 2 +429847 3 +434120 3 +435134 3 +436275 2 +437464 2 +438532 2 +439570 3 +440741 3 +445172 3 +446227 2 +447300 3 +448308 2 +449463 3 +450506 2 +451702 2 +455483 2 +456547 3 +457580 3 +458632 2 +459791 3 +460793 3 +461883 2 +462947 2 +467055 2 +468236 3 +469284 2 +470462 3 +471522 2 +472695 3 +473768 2 +474871 3 +478995 2 +480109 2 +481235 2 +482276 3 +483436 3 +484571 3 +485631 3 +490190 2 +491336 3 +492365 3 +493536 3 +494726 3 +495775 2 +496915 2 +497980 2 +502476 3 +503637 3 +504711 3 +505843 2 +506955 3 +507997 2 +509050 2 +513770 2 +514818 2 +515971 3 +517151 2 +518269 3 +519418 3 +520471 2 +524054 3 +525121 3 +526307 3 +527419 2 +528459 2 +529518 2 +530699 2 +531881 3 +535552 3 +536615 2 +537655 2 +538751 3 +539906 3 +540971 2 +542117 3 +546543 3 +547643 3 +548651 2 +549707 3 +550787 2 +551876 3 +552911 2 +557991 2 +559173 2 +560253 3 +561357 2 +562407 3 +563447 2 +564494 3 +565511 3 +569931 2 +571100 3 +572227 2 +573334 2 +574441 2 +575538 3 +576694 3 +577788 2 +581469 2 +582512 3 +583576 2 +584593 2 +585707 3 +586787 3 +587913 3 +591882 2 +592998 2 +594180 2 +595273 3 +596307 3 +597391 2 +598487 3 +599556 3 +603402 2 +604495 2 +605565 2 +606608 3 +607784 2 +608946 3 +610101 2 +614242 3 +615319 3 +616495 2 +617627 3 +618692 2 +619824 2 +620923 3 +622077 3 +625712 3 +626755 2 +627943 2 +628976 2 +630093 3 +631115 3 +632194 2 +633269 2 +636989 2 +637993 2 +639035 3 +640216 2 +641314 2 +642461 3 +643545 3 +648028 3 +649063 2 +650179 3 +651364 2 +652449 3 +653617 2 +654779 3 +659006 3 +660123 3 +661274 3 +662347 2 +663447 2 +664575 2 +665595 3 +666767 3 +670592 3 +671650 2 +672653 2 +673738 3 +674772 3 +675879 3 +677048 2 +678171 2 +681869 2 +682963 2 +683970 3 +685084 3 +686135 2 +687228 3 +688419 3 +692033 3 +693072 3 +694247 2 +695303 2 +696433 3 +697551 3 +698703 2 +699823 3 +704375 3 +705529 2 +706637 2 +707682 3 +708801 2 +709805 2 +710883 2 +714608 2 +715672 2 +716861 3 +718033 3 +719086 2 +720221 3 +721270 3 +725512 3 +726525 2 +727693 3 +728755 2 +729826 2 +731004 2 +732111 3 +735821 3 +736896 2 +737991 3 +739049 3 +740228 2 +741338 2 +742417 2 +743612 2 +748334 2 +749481 2 +750580 3 +751631 3 +752789 2 +753954 3 +755145 3 +756236 3 +780615 2 +781666 2 +782742 3 +783904 3 +785013 3 +786040 3 +787096 2 +788187 2 +792335 3 +793385 2 +794385 2 +795396 3 +796433 3 +797452 2 +798475 2 +802763 2 +803770 2 +804812 3 +805970 2 +807136 3 +808184 3 +809241 3 +813648 2 +814744 3 +815823 3 +816982 2 +818053 2 +819239 2 +820305 3 +821357 3 +825178 3 +826186 2 +827243 2 +828376 2 +829444 3 +830528 2 +831584 3 +835661 3 +836729 3 +837877 2 +839001 3 +840033 2 +841075 2 +842271 2 +843383 3 +847592 3 +848668 3 +849683 3 +850691 2 +851723 2 +852776 2 +853882 2 +857662 3 +858821 3 +859922 3 +861050 2 +862113 2 +863200 3 +864382 2 +865518 3 +869357 2 +870521 3 +871641 3 +872678 3 +873797 2 +874804 2 +875909 3 +877042 3 +880822 3 +881985 2 +883129 2 +884172 3 +885224 2 +886318 2 +887460 3 +891240 3 +892297 2 +893432 2 +894571 2 +895760 3 +896769 3 +897823 2 +902267 2 +903373 2 +904538 3 +905656 3 +906708 3 +907799 2 +908896 2 +910027 3 +913822 2 +914916 2 +915992 2 +917177 2 +918188 3 +919328 3 +920372 3 +924769 2 +925803 2 +926892 3 +927988 3 +929091 2 +930102 3 +931192 2 +932222 3 +936485 3 +937638 3 +938815 3 +940001 2 +941127 2 +942180 3 +943295 2 +944446 2 +948593 3 +949785 2 +950908 2 +952070 3 +953099 3 +954174 2 +955263 3 +959097 2 +960156 3 +961222 3 +962419 2 +963439 3 +964440 3 +965516 2 +969220 2 +970391 2 +971502 2 +972563 3 +973598 3 +974781 3 +975904 2 +977014 2 +980716 3 +981783 3 +982867 2 +984006 2 +985042 3 +986102 3 +987272 3 +988411 2 +992396 2 +993476 3 +994523 2 +995623 3 +996764 2 +997921 3 +999019 2 +1003100 3 +1004299 3 +1005364 2 +1006555 2 +1007642 2 +1008667 3 +1009712 2 +1010733 2 +1014451 2 +1015600 3 +1016729 3 +1017818 2 +1018882 3 +1020044 2 +1021149 2 +1022172 3 +1026317 2 +1027365 2 +1028378 3 +1029396 3 +1030420 3 +1031494 3 +1032685 3 +1036698 2 +1037877 3 +1038915 3 +1039946 2 +1041079 2 +1042221 2 +1043335 3 +1047931 2 +1048977 3 +1050122 3 +1051289 2 +1052398 3 +1053590 3 +1054603 2 +1055671 3 +1060208 2 +1061313 2 +1062437 2 +1063447 3 +1064565 2 +1065698 2 +1066798 3 +1071341 2 +1072482 3 +1073495 2 +1074641 3 +1075657 3 +1076745 3 +1077811 2 +1081501 3 +1082553 3 +1083558 2 +1084746 2 +1085801 2 +1086890 3 +1087958 2 +1089066 3 +1092875 2 +1093906 3 +1094985 2 +1096103 3 +1097240 2 +1098309 3 +1099492 2 +1100673 2 +1104513 2 +1105575 3 +1106600 3 +1107712 2 +1108836 3 +1109929 2 +1110980 3 +1116029 3 +1117067 3 +1118160 3 +1119304 2 +1120464 3 +1121649 3 +1122769 2 +1123814 2 +1127982 2 +1129080 3 +1130098 2 +1131143 2 +1132143 3 +1133146 2 +1134327 3 +1139244 3 +1140440 2 +1141560 2 +1142632 3 +1143739 2 +1144887 2 +1146016 3 +1149611 3 +1150749 2 +1151798 3 +1152924 3 +1154011 3 +1155106 2 +1156151 2 +1157212 2 +1160791 2 +1161988 2 +1163075 3 +1164127 3 +1165209 2 +1166304 2 +1167426 3 +1171136 2 +1172309 3 +1173467 3 +1174598 2 +1175776 3 +1176896 3 +1177942 3 +1179027 2 +1183233 2 +1184343 3 +1185519 3 +1186620 2 +1187701 2 +1188718 2 +1189738 3 +1193513 3 +1194552 2 +1195728 2 +1196839 2 +1197909 3 +1198939 3 +1200138 3 +1201210 3 +1204914 3 +1205935 3 +1207050 2 +1208112 2 +1209248 2 +1210326 3 +1211511 2 +1212588 2 +1217014 2 +1218167 3 +1219202 3 +1220276 2 +1221405 2 +1222497 3 +1223606 3 +1228398 3 +1229515 2 +1230555 2 +1231596 3 +1232748 3 +1233775 2 +1234949 3 +1238694 3 +1239772 2 +1240813 2 +1241859 3 +1242927 2 +1243959 2 +1245003 2 +1246106 3 +1249968 2 +1251100 3 +1252276 2 +1253367 2 +1254483 3 +1255590 3 +1256664 2 +1257684 2 +1262431 3 +1263528 2 +1264661 2 +1265821 3 +1267009 3 +1268089 3 +1269212 3 +1273013 2 +1274124 2 +1275304 2 +1276442 3 +1277636 3 +1278673 2 +1279838 3 +1280993 3 +1285105 2 +1286125 2 +1287247 2 +1288372 2 +1289402 3 +1290591 3 +1291592 3 +1295219 3 +1296347 3 +1297510 2 +1298598 2 +1299757 3 +1300766 2 +1301875 3 +1305703 2 +1306714 3 +1307855 3 +1309050 2 +1310185 3 +1311276 2 +1312322 3 +1313374 2 +1317969 2 +1319166 3 +1320339 3 +1321412 3 +1322585 3 +1323663 3 +1324740 2 +1329828 2 +1331021 2 +1332033 3 +1333087 2 +1334206 3 +1335210 3 +1336318 2 +1340951 3 +1342047 3 +1343240 2 +1344331 3 +1345464 2 +1346580 2 +1347666 2 +1348853 3 +1352542 3 +1353585 2 +1354607 2 +1355627 2 +1356821 3 +1357920 2 +1359093 2 +1360247 3 +1364982 3 +1366079 3 +1367191 2 +1368309 3 +1369410 3 +1370615 3 +1371687 2 +1376331 3 +1377358 2 +1378416 2 +1379428 2 +1380599 2 +1381774 3 +1382942 2 +1387490 2 +1388503 3 +1389636 3 +1390724 2 +1391754 2 +1392935 2 +1394057 3 +1395098 3 +1398855 3 +1399974 2 +1401049 2 +1402107 3 +1403192 2 +1404376 3 +1405503 2 +1406577 3 +1410597 2 +1411662 3 +1412802 2 +1413861 3 +1414895 2 +1415897 3 +1416930 2 +1418002 2 +1422056 2 +1423136 2 +1424138 3 +1425206 3 +1426406 2 +1427578 3 +1428687 2 +1432350 2 +1433464 3 +1434598 3 +1435758 2 +1436912 3 +1438057 3 +1439104 2 +1443441 2 +1444457 3 +1445492 2 +1446578 3 +1447675 2 +1448762 3 +1449783 3 +1450825 3 +1454440 3 +1455506 2 +1456675 3 +1457859 2 +1459048 3 +1460167 2 +1461348 2 +1465237 3 +1466320 2 +1467470 3 +1468577 3 +1469710 2 +1470774 2 +1471958 2 +1473030 2 +1477489 2 +1478547 3 +1479563 3 +1480655 2 +1481699 3 +1482758 3 +1483833 2 +1487672 3 +1488692 3 +1489789 2 +1490874 2 +1492061 3 +1493207 3 +1494278 3 +1495341 2 +1504231 2 +1505411 3 +1506542 2 +1507565 3 +1508707 2 +1509794 2 +1510833 3 +1515496 2 +1516648 3 +1517730 3 +1518837 2 +1519956 2 +1521029 3 +1522121 3 +1523276 3 +1527115 3 +1528188 2 +1529219 2 +1530283 3 +1531411 3 +1532614 2 +1533746 2 +1534813 3 +1538494 3 +1539613 3 +1540660 2 +1541851 2 +1543037 2 +1544132 3 +1545321 2 +1549231 2 +1550297 3 +1551309 3 +1552446 2 +1553616 3 +1554815 2 +1555887 2 +1559459 2 +1560528 3 +1561549 2 +1562733 2 +1563884 3 +1564976 2 +1566079 3 +1569857 3 +1570947 2 +1572040 2 +1573128 2 +1574249 2 +1575336 3 +1576359 3 +1577369 3 +1580936 2 +1582093 3 +1583114 3 +1584210 2 +1585282 3 +1586384 3 +1587420 3 +1588497 2 +1592553 3 +1593669 2 +1594828 3 +1595993 3 +1597132 2 +1598302 2 +1599451 2 +1603163 2 +1604292 3 +1605382 3 +1606408 2 +1607463 3 +1608533 2 +1609628 3 +1613882 2 +1614897 3 +1615906 3 +1617002 2 +1618048 2 +1619117 3 +1620141 3 +1621178 2 +1625341 2 +1626409 3 +1627471 2 +1628497 2 +1629523 2 +1630555 3 +1631641 3 +1632661 3 +1636949 3 +1638057 3 +1639211 2 +1640407 3 +1641411 2 +1642550 2 +1643726 3 +1647529 3 +1648540 2 +1649709 3 +1650834 3 +1651866 2 +1652965 2 +1654006 2 +1655168 3 +1659242 3 +1660284 3 +1661351 2 +1662406 2 +1663506 3 +1664550 2 +1665550 2 +1669187 2 +1670228 3 +1671260 3 +1672281 2 +1673421 3 +1674540 3 +1675634 2 +1676775 2 +1681120 3 +1682151 3 +1683159 3 +1684254 2 +1685391 2 +1686504 2 +1687584 3 +1688592 3 +1692988 2 +1694027 3 +1695100 2 +1696284 3 +1697385 3 +1698389 2 +1699479 3 +1703511 3 +1704582 2 +1705694 3 +1706704 2 +1707851 3 +1709006 2 +1710181 2 +1711344 2 +1715229 3 +1716265 3 +1717379 2 +1718387 2 +1719439 3 +1720592 2 +1721738 2 +1725521 3 +1726650 3 +1727680 2 +1728731 3 +1729833 2 +1730921 2 +1732112 2 +1736712 2 +1737763 3 +1738801 3 +1739884 3 +1740901 3 +1741905 2 +1742970 2 +1744114 3 +1747980 2 +1749040 3 +1750145 2 +1751158 2 +1752210 3 +1753348 3 +1754454 3 +1758239 2 +1759261 2 +1760349 3 +1761408 3 +1762507 3 +1763699 2 +1764760 2 +1765895 2 +1769765 2 +1770794 3 +1771918 2 +1772982 2 +1774105 3 +1775256 2 +1776332 2 +1780333 3 +1781523 2 +1782676 2 +1783711 3 +1784762 3 +1785776 3 +1786974 3 +1788015 2 +1792797 3 +1793817 2 +1794927 2 +1795979 3 +1797138 3 +1798195 3 +1799271 2 +1800447 2 +1804362 2 +1805487 3 +1806627 3 +1807734 2 +1808863 3 +1810030 2 +1811147 3 +1815673 2 +1816768 3 +1817887 2 +1819000 2 +1820070 3 +1821080 2 +1822266 3 +1823369 3 +1827477 2 +1828590 3 +1829667 3 +1830742 2 +1831894 3 +1833075 2 +1834185 2 +1837981 3 +1839072 3 +1840097 3 +1841249 2 +1842261 2 +1843348 3 +1844542 3 +1848955 2 +1850085 2 +1851180 2 +1852310 2 +1853416 3 +1854593 3 +1855649 3 +1856745 2 +1860997 2 +1862158 3 +1863171 2 +1864303 3 +1865327 2 +1866477 3 +1867650 3 +1872104 2 +1873252 3 +1874284 2 +1875322 2 +1876520 2 +1877677 3 +1878815 3 +1879862 3 +1884280 3 +1885380 2 +1886515 3 +1887522 2 +1888614 2 +1889712 3 +1890892 2 +1892052 2 +1895771 2 +1896968 2 +1898102 3 +1899245 2 +1900422 3 +1901524 3 +1902683 3 +1906916 2 +1908026 3 +1909197 2 +1910270 2 +1911322 3 +1912409 3 +1913559 2 +1914755 2 +1918311 2 +1919427 3 +1920471 3 +1921580 3 +1922743 2 +1923860 3 +1924934 2 +1926062 2 +1930454 2 +1931607 2 +1932652 3 +1933658 3 +1934779 3 +1935873 2 +1936919 3 +1941002 3 +1942192 3 +1943193 2 +1944276 2 +1945304 3 +1946444 3 +1947637 2 +1951356 3 +1952551 3 +1953682 2 +1954716 2 +1955720 2 +1956898 2 +1958028 3 +1959106 3 +1963806 2 +1964875 2 +1965920 3 +1967106 2 +1968139 3 +1969296 3 +1970441 2 +1974001 3 +1975105 2 +1976158 3 +1977269 3 +1978388 2 +1979397 3 +1980512 2 +1981689 2 +1986207 2 +1987215 2 +1988403 2 +1989472 3 +1990517 3 +1991517 3 +1992687 3 +1996578 2 +1997751 3 +1998914 2 +1999947 3 +2000972 3 +2002150 2 +2003335 2 +2004472 2 +2008379 2 +2009455 2 +2010504 3 +2011650 2 +2012663 3 +2013758 3 +2014901 3 +2018547 3 +2019724 2 +2020859 3 +2021985 2 +2023157 3 +2024342 2 +2025481 3 +2029142 3 +2030338 2 +2031389 3 +2032455 3 +2033496 2 +2034665 2 +2035853 2 +2036857 3 +2041484 3 +2042668 2 +2043745 3 +2044795 2 +2045971 2 +2046998 3 +2048039 3 +2051921 2 +2053094 2 +2054233 2 +2055353 2 +2056464 3 +2057572 2 +2058755 3 +2059911 3 +2064430 3 +2065490 3 +2066499 2 +2067564 2 +2068710 2 +2069769 3 +2070824 3 +2071936 3 +2076572 3 +2077733 3 +2078748 2 +2079924 3 +2080945 2 +2082026 2 +2083166 2 +2086738 2 +2087841 2 +2088990 2 +2090184 3 +2091335 2 +2092518 3 +2093711 3 +2094739 3 +2099365 3 +2100478 2 +2101562 2 +2102565 2 +2103652 3 +2104755 3 +2105915 3 +2107055 3 +2111319 3 +2112332 2 +2113464 3 +2114517 2 +2115612 3 +2116732 3 +2117759 2 +2122229 2 +2123417 2 +2124450 2 +2125595 3 +2126673 3 +2127798 2 +2128812 2 +2132445 3 +2133483 3 +2134680 2 +2135814 3 +2136878 3 +2138080 3 +2139135 2 +2140266 2 +2143949 3 +2145050 2 +2146081 3 +2147147 3 +2148325 2 +2149505 2 +2150604 3 +2154266 2 +2155326 3 +2156442 2 +2157625 3 +2158724 2 +2159732 3 +2160761 2 +2165362 3 +2166505 3 +2167701 2 +2168829 2 +2169836 3 +2171001 2 +2172083 2 +2173190 2 +2177595 2 +2178711 3 +2179836 2 +2180953 3 +2182150 2 +2183201 2 +2184226 2 +2185310 3 +2189669 2 +2190792 3 +2191847 3 +2192936 3 +2193994 2 +2195087 2 +2196159 2 +2200409 3 +2201552 3 +2202649 2 +2203656 2 +2204828 3 +2205865 2 +2207064 3 +2211341 3 +2212534 3 +2213704 2 +2214715 3 +2215779 3 +2216971 3 +2218058 2 +2219209 2 +40759 5 +41903 5 +42979 4 +44089 4 +45209 4 +46231 5 +47246 4 +48381 5 +52148 5 +53245 4 +54360 5 +55364 4 +56444 4 +57589 4 +58601 5 +63149 4 +64217 4 +65231 5 +66272 5 +67348 5 +68471 4 +69652 4 +73740 5 +74880 5 +75948 5 +77005 4 +78116 4 +79305 5 +80481 5 +81558 4 +85079 5 +86153 4 +87211 5 +88411 4 +89505 4 +90664 4 +91762 4 +95635 5 +96809 5 +97836 5 +98911 5 +99983 4 +101155 4 +102214 4 +105955 5 +107111 4 +108113 5 +109195 5 +110295 4 +111355 5 +112434 4 +113504 5 +117990 4 +119029 4 +120137 4 +121222 5 +122387 5 +123486 5 +124665 4 +125692 5 +129126 4 +130310 5 +131341 4 +132412 5 +133587 4 +134750 5 +135805 4 +139662 4 +140751 5 +141879 5 +142914 5 +144013 5 +145104 4 +146262 5 +147429 4 +151092 4 +152184 4 +153310 5 +154444 5 +155615 5 +156653 4 +157721 4 +161796 4 +162848 5 +163952 4 +165114 5 +166231 4 +167291 5 +168381 4 +169417 5 +173628 5 +174674 5 +175859 5 +177036 5 +178125 4 +179266 4 +180371 4 +181404 5 +185094 4 +186112 4 +187183 4 +188305 4 +189335 4 +190410 5 +191560 5 +192615 5 +196720 5 +197780 4 +198878 4 +199963 5 +201106 4 +202117 4 +203186 5 +206908 4 +208035 5 +209192 4 +210289 5 +211388 5 +212411 4 +213589 5 +218038 5 +219130 5 +220245 4 +221288 4 +222483 5 +223487 5 +224549 5 +225698 4 +230629 5 +231663 4 +232752 5 +233793 5 +234966 5 +236101 4 +237180 4 +241433 4 +242612 4 +243777 5 +244912 4 +246037 5 +247229 5 +248389 4 +252343 4 +253368 5 +254424 4 +255505 4 +256587 5 +257613 4 +258672 4 +259802 5 +264310 4 +265327 5 +266521 4 +267565 5 +268687 5 +269700 5 +270712 4 +271861 5 +276340 4 +277435 4 +278444 4 +279583 4 +280713 4 +281833 5 +283001 5 +287510 5 +288536 5 +289569 4 +290626 4 +291800 4 +292997 5 +294047 5 +297820 5 +298990 5 +300026 4 +301221 5 +302319 4 +303357 4 +304509 4 +305556 5 +309506 4 +310698 5 +311758 4 +312889 5 +314069 5 +315081 4 +316101 5 +320683 4 +321850 4 +322920 4 +324015 5 +325165 4 +326211 4 +327334 5 +328503 5 +332409 4 +333569 5 +334705 4 +335787 5 +336961 5 +338118 4 +339227 5 +340293 5 +344329 5 +345377 4 +346395 5 +347397 4 +348430 5 +349466 4 +350485 4 +354348 4 +355438 5 +356520 5 +357587 4 +358663 5 +359674 4 +360765 4 +361924 5 +365880 4 +366937 4 +368064 5 +369177 4 +370328 4 +371516 5 +372648 4 +376503 5 +377562 5 +378697 4 +379833 4 +380873 5 +381932 5 +383020 5 +386705 5 +387775 4 +388890 5 +389917 4 +390973 5 +392170 4 +393183 4 +394241 5 +398880 5 +399928 5 +401060 4 +402163 4 +403291 4 +404318 4 +405405 5 +406480 5 +410809 4 +411976 5 +413172 5 +414370 4 +415482 4 +416664 4 +417729 5 +418881 5 +422962 5 +424044 4 +425212 4 +426387 5 +427579 4 +428714 4 +429847 5 +434120 5 +435134 5 +436275 4 +437464 4 +438532 4 +439570 5 +440741 5 +445172 5 +446227 4 +447300 5 +448308 4 +449463 5 +450506 4 +451702 4 +455483 4 +456547 5 +457580 5 +458632 4 +459791 5 +460793 5 +461883 4 +462947 4 +467055 4 +468236 5 +469284 4 +470462 5 +471522 4 +472695 5 +473768 4 +474871 5 +478995 4 +480109 4 +481235 4 +482276 5 +483436 5 +484571 5 +485631 5 +490190 4 +491336 5 +492365 5 +493536 5 +494726 5 +495775 4 +496915 4 +497980 4 +502476 5 +503637 5 +504711 5 +505843 4 +506955 5 +507997 4 +509050 4 +513770 4 +514818 4 +515971 5 +517151 4 +518269 5 +519418 5 +520471 4 +524054 5 +525121 5 +526307 5 +527419 4 +528459 4 +529518 4 +530699 4 +531881 5 +535552 5 +536615 4 +537655 4 +538751 5 +539906 5 +540971 4 +542117 5 +546543 5 +547643 5 +548651 4 +549707 5 +550787 4 +551876 5 +552911 4 +557991 4 +559173 4 +560253 5 +561357 4 +562407 5 +563447 4 +564494 5 +565511 5 +569931 4 +571100 5 +572227 4 +573334 4 +574441 4 +575538 5 +576694 5 +577788 4 +581469 4 +582512 5 +583576 4 +584593 4 +585707 5 +586787 5 +587913 5 +591882 4 +592998 4 +594180 4 +595273 5 +596307 5 +597391 4 +598487 5 +599556 5 +603402 4 +604495 4 +605565 4 +606608 5 +607784 4 +608946 5 +610101 4 +614242 5 +615319 5 +616495 4 +617627 5 +618692 4 +619824 4 +620923 5 +622077 5 +625712 5 +626755 4 +627943 4 +628976 4 +630093 5 +631115 5 +632194 4 +633269 4 +636989 4 +637993 4 +639035 5 +640216 4 +641314 4 +642461 5 +643545 5 +648028 5 +649063 4 +650179 5 +651364 4 +652449 5 +653617 4 +654779 5 +659006 5 +660123 5 +661274 5 +662347 4 +663447 4 +664575 4 +665595 5 +666767 5 +670592 5 +671650 4 +672653 4 +673738 5 +674772 5 +675879 5 +677048 4 +678171 4 +681869 4 +682963 4 +683970 5 +685084 5 +686135 4 +687228 5 +688419 5 +692033 5 +693072 5 +694247 4 +695303 4 +696433 5 +697551 5 +698703 4 +699823 5 +704375 5 +705529 4 +706637 4 +707682 5 +708801 4 +709805 4 +710883 4 +714608 4 +715672 4 +716861 5 +718033 5 +719086 4 +720221 5 +721270 5 +725512 5 +726525 4 +727693 5 +728755 4 +729826 4 +731004 4 +732111 5 +735821 5 +736896 4 +737991 5 +739049 5 +740228 4 +741338 4 +742417 4 +743612 4 +748334 4 +749481 4 +750580 5 +751631 5 +752789 4 +753954 5 +755145 5 +756236 5 +780615 4 +781666 4 +782742 5 +783904 5 +785013 5 +786040 5 +787096 4 +788187 4 +792335 5 +793385 4 +794385 4 +795396 5 +796433 5 +797452 4 +798475 4 +802763 4 +803770 4 +804812 5 +805970 4 +807136 5 +808184 5 +809241 5 +813648 4 +814744 5 +815823 5 +816982 4 +818053 4 +819239 4 +820305 5 +821357 5 +825178 5 +826186 4 +827243 4 +828376 4 +829444 5 +830528 4 +831584 5 +835661 5 +836729 5 +837877 4 +839001 5 +840033 4 +841075 4 +842271 4 +843383 5 +847592 5 +848668 5 +849683 5 +850691 4 +851723 4 +852776 4 +853882 4 +857662 5 +858821 5 +859922 5 +861050 4 +862113 4 +863200 5 +864382 4 +865518 5 +869357 4 +870521 5 +871641 5 +872678 5 +873797 4 +874804 4 +875909 5 +877042 5 +880822 5 +881985 4 +883129 4 +884172 5 +885224 4 +886318 4 +887460 5 +891240 5 +892297 4 +893432 4 +894571 4 +895760 5 +896769 5 +897823 4 +902267 4 +903373 4 +904538 5 +905656 5 +906708 5 +907799 4 +908896 4 +910027 5 +913822 4 +914916 4 +915992 4 +917177 4 +918188 5 +919328 5 +920372 5 +924769 4 +925803 4 +926892 5 +927988 5 +929091 4 +930102 5 +931192 4 +932222 5 +936485 5 +937638 5 +938815 5 +940001 4 +941127 4 +942180 5 +943295 4 +944446 4 +948593 5 +949785 4 +950908 4 +952070 5 +953099 5 +954174 4 +955263 5 +959097 4 +960156 5 +961222 5 +962419 4 +963439 5 +964440 5 +965516 4 +969220 4 +970391 4 +971502 4 +972563 5 +973598 5 +974781 5 +975904 4 +977014 4 +980716 5 +981783 5 +982867 4 +984006 4 +985042 5 +986102 5 +987272 5 +988411 4 +992396 4 +993476 5 +994523 4 +995623 5 +996764 4 +997921 5 +999019 4 +1003100 5 +1004299 5 +1005364 4 +1006555 4 +1007642 4 +1008667 5 +1009712 4 +1010733 4 +1014451 4 +1015600 5 +1016729 5 +1017818 4 +1018882 5 +1020044 4 +1021149 4 +1022172 5 +1026317 4 +1027365 4 +1028378 5 +1029396 5 +1030420 5 +1031494 5 +1032685 5 +1036698 4 +1037877 5 +1038915 5 +1039946 4 +1041079 4 +1042221 4 +1043335 5 +1047931 4 +1048977 5 +1050122 5 +1051289 4 +1052398 5 +1053590 5 +1054603 4 +1055671 5 +1060208 4 +1061313 4 +1062437 4 +1063447 5 +1064565 4 +1065698 4 +1066798 5 +1071341 4 +1072482 5 +1073495 4 +1074641 5 +1075657 5 +1076745 5 +1077811 4 +1081501 5 +1082553 5 +1083558 4 +1084746 4 +1085801 4 +1086890 5 +1087958 4 +1089066 5 +1092875 4 +1093906 5 +1094985 4 +1096103 5 +1097240 4 +1098309 5 +1099492 4 +1100673 4 +1104513 4 +1105575 5 +1106600 5 +1107712 4 +1108836 5 +1109929 4 +1110980 5 +1116029 5 +1117067 5 +1118160 5 +1119304 4 +1120464 5 +1121649 5 +1122769 4 +1123814 4 +1127982 4 +1129080 5 +1130098 4 +1131143 4 +1132143 5 +1133146 4 +1134327 5 +1139244 5 +1140440 4 +1141560 4 +1142632 5 +1143739 4 +1144887 4 +1146016 5 +1149611 5 +1150749 4 +1151798 5 +1152924 5 +1154011 5 +1155106 4 +1156151 4 +1157212 4 +1160791 4 +1161988 4 +1163075 5 +1164127 5 +1165209 4 +1166304 4 +1167426 5 +1171136 4 +1172309 5 +1173467 5 +1174598 4 +1175776 5 +1176896 5 +1177942 5 +1179027 4 +1183233 4 +1184343 5 +1185519 5 +1186620 4 +1187701 4 +1188718 4 +1189738 5 +1193513 5 +1194552 4 +1195728 4 +1196839 4 +1197909 5 +1198939 5 +1200138 5 +1201210 5 +1204914 5 +1205935 5 +1207050 4 +1208112 4 +1209248 4 +1210326 5 +1211511 4 +1212588 4 +1217014 4 +1218167 5 +1219202 5 +1220276 4 +1221405 4 +1222497 5 +1223606 5 +1228398 5 +1229515 4 +1230555 4 +1231596 5 +1232748 5 +1233775 4 +1234949 5 +1238694 5 +1239772 4 +1240813 4 +1241859 5 +1242927 4 +1243959 4 +1245003 4 +1246106 5 +1249968 4 +1251100 5 +1252276 4 +1253367 4 +1254483 5 +1255590 5 +1256664 4 +1257684 4 +1262431 5 +1263528 4 +1264661 4 +1265821 5 +1267009 5 +1268089 5 +1269212 5 +1273013 4 +1274124 4 +1275304 4 +1276442 5 +1277636 5 +1278673 4 +1279838 5 +1280993 5 +1285105 4 +1286125 4 +1287247 4 +1288372 4 +1289402 5 +1290591 5 +1291592 5 +1295219 5 +1296347 5 +1297510 4 +1298598 4 +1299757 5 +1300766 4 +1301875 5 +1305703 4 +1306714 5 +1307855 5 +1309050 4 +1310185 5 +1311276 4 +1312322 5 +1313374 4 +1317969 4 +1319166 5 +1320339 5 +1321412 5 +1322585 5 +1323663 5 +1324740 4 +1329828 4 +1331021 4 +1332033 5 +1333087 4 +1334206 5 +1335210 5 +1336318 4 +1340951 5 +1342047 5 +1343240 4 +1344331 5 +1345464 4 +1346580 4 +1347666 4 +1348853 5 +1352542 5 +1353585 4 +1354607 4 +1355627 4 +1356821 5 +1357920 4 +1359093 4 +1360247 5 +1364982 5 +1366079 5 +1367191 4 +1368309 5 +1369410 5 +1370615 5 +1371687 4 +1376331 5 +1377358 4 +1378416 4 +1379428 4 +1380599 4 +1381774 5 +1382942 4 +1387490 4 +1388503 5 +1389636 5 +1390724 4 +1391754 4 +1392935 4 +1394057 5 +1395098 5 +1398855 5 +1399974 4 +1401049 4 +1402107 5 +1403192 4 +1404376 5 +1405503 4 +1406577 5 +1410597 4 +1411662 5 +1412802 4 +1413861 5 +1414895 4 +1415897 5 +1416930 4 +1418002 4 +1422056 4 +1423136 4 +1424138 5 +1425206 5 +1426406 4 +1427578 5 +1428687 4 +1432350 4 +1433464 5 +1434598 5 +1435758 4 +1436912 5 +1438057 5 +1439104 4 +1443441 4 +1444457 5 +1445492 4 +1446578 5 +1447675 4 +1448762 5 +1449783 5 +1450825 5 +1454440 5 +1455506 4 +1456675 5 +1457859 4 +1459048 5 +1460167 4 +1461348 4 +1465237 5 +1466320 4 +1467470 5 +1468577 5 +1469710 4 +1470774 4 +1471958 4 +1473030 4 +1477489 4 +1478547 5 +1479563 5 +1480655 4 +1481699 5 +1482758 5 +1483833 4 +1487672 5 +1488692 5 +1489789 4 +1490874 4 +1492061 5 +1493207 5 +1494278 5 +1495341 4 +1504231 4 +1505411 5 +1506542 4 +1507565 5 +1508707 4 +1509794 4 +1510833 5 +1515496 4 +1516648 5 +1517730 5 +1518837 4 +1519956 4 +1521029 5 +1522121 5 +1523276 5 +1527115 5 +1528188 4 +1529219 4 +1530283 5 +1531411 5 +1532614 4 +1533746 4 +1534813 5 +1538494 5 +1539613 5 +1540660 4 +1541851 4 +1543037 4 +1544132 5 +1545321 4 +1549231 4 +1550297 5 +1551309 5 +1552446 4 +1553616 5 +1554815 4 +1555887 4 +1559459 4 +1560528 5 +1561549 4 +1562733 4 +1563884 5 +1564976 4 +1566079 5 +1569857 5 +1570947 4 +1572040 4 +1573128 4 +1574249 4 +1575336 5 +1576359 5 +1577369 5 +1580936 4 +1582093 5 +1583114 5 +1584210 4 +1585282 5 +1586384 5 +1587420 5 +1588497 4 +1592553 5 +1593669 4 +1594828 5 +1595993 5 +1597132 4 +1598302 4 +1599451 4 +1603163 4 +1604292 5 +1605382 5 +1606408 4 +1607463 5 +1608533 4 +1609628 5 +1613882 4 +1614897 5 +1615906 5 +1617002 4 +1618048 4 +1619117 5 +1620141 5 +1621178 4 +1625341 4 +1626409 5 +1627471 4 +1628497 4 +1629523 4 +1630555 5 +1631641 5 +1632661 5 +1636949 5 +1638057 5 +1639211 4 +1640407 5 +1641411 4 +1642550 4 +1643726 5 +1647529 5 +1648540 4 +1649709 5 +1650834 5 +1651866 4 +1652965 4 +1654006 4 +1655168 5 +1659242 5 +1660284 5 +1661351 4 +1662406 4 +1663506 5 +1664550 4 +1665550 4 +1669187 4 +1670228 5 +1671260 5 +1672281 4 +1673421 5 +1674540 5 +1675634 4 +1676775 4 +1681120 5 +1682151 5 +1683159 5 +1684254 4 +1685391 4 +1686504 4 +1687584 5 +1688592 5 +1692988 4 +1694027 5 +1695100 4 +1696284 5 +1697385 5 +1698389 4 +1699479 5 +1703511 5 +1704582 4 +1705694 5 +1706704 4 +1707851 5 +1709006 4 +1710181 4 +1711344 4 +1715229 5 +1716265 5 +1717379 4 +1718387 4 +1719439 5 +1720592 4 +1721738 4 +1725521 5 +1726650 5 +1727680 4 +1728731 5 +1729833 4 +1730921 4 +1732112 4 +1736712 4 +1737763 5 +1738801 5 +1739884 5 +1740901 5 +1741905 4 +1742970 4 +1744114 5 +1747980 4 +1749040 5 +1750145 4 +1751158 4 +1752210 5 +1753348 5 +1754454 5 +1758239 4 +1759261 4 +1760349 5 +1761408 5 +1762507 5 +1763699 4 +1764760 4 +1765895 4 +1769765 4 +1770794 5 +1771918 4 +1772982 4 +1774105 5 +1775256 4 +1776332 4 +1780333 5 +1781523 4 +1782676 4 +1783711 5 +1784762 5 +1785776 5 +1786974 5 +1788015 4 +1792797 5 +1793817 4 +1794927 4 +1795979 5 +1797138 5 +1798195 5 +1799271 4 +1800447 4 +1804362 4 +1805487 5 +1806627 5 +1807734 4 +1808863 5 +1810030 4 +1811147 5 +1815673 4 +1816768 5 +1817887 4 +1819000 4 +1820070 5 +1821080 4 +1822266 5 +1823369 5 +1827477 4 +1828590 5 +1829667 5 +1830742 4 +1831894 5 +1833075 4 +1834185 4 +1837981 5 +1839072 5 +1840097 5 +1841249 4 +1842261 4 +1843348 5 +1844542 5 +1848955 4 +1850085 4 +1851180 4 +1852310 4 +1853416 5 +1854593 5 +1855649 5 +1856745 4 +1860997 4 +1862158 5 +1863171 4 +1864303 5 +1865327 4 +1866477 5 +1867650 5 +1872104 4 +1873252 5 +1874284 4 +1875322 4 +1876520 4 +1877677 5 +1878815 5 +1879862 5 +1884280 5 +1885380 4 +1886515 5 +1887522 4 +1888614 4 +1889712 5 +1890892 4 +1892052 4 +1895771 4 +1896968 4 +1898102 5 +1899245 4 +1900422 5 +1901524 5 +1902683 5 +1906916 4 +1908026 5 +1909197 4 +1910270 4 +1911322 5 +1912409 5 +1913559 4 +1914755 4 +1918311 4 +1919427 5 +1920471 5 +1921580 5 +1922743 4 +1923860 5 +1924934 4 +1926062 4 +1930454 4 +1931607 4 +1932652 5 +1933658 5 +1934779 5 +1935873 4 +1936919 5 +1941002 5 +1942192 5 +1943193 4 +1944276 4 +1945304 5 +1946444 5 +1947637 4 +1951356 5 +1952551 5 +1953682 4 +1954716 4 +1955720 4 +1956898 4 +1958028 5 +1959106 5 +1963806 4 +1964875 4 +1965920 5 +1967106 4 +1968139 5 +1969296 5 +1970441 4 +1974001 5 +1975105 4 +1976158 5 +1977269 5 +1978388 4 +1979397 5 +1980512 4 +1981689 4 +1986207 4 +1987215 4 +1988403 4 +1989472 5 +1990517 5 +1991517 5 +1992687 5 +1996578 4 +1997751 5 +1998914 4 +1999947 5 +2000972 5 +2002150 4 +2003335 4 +2004472 4 +2008379 4 +2009455 4 +2010504 5 +2011650 4 +2012663 5 +2013758 5 +2014901 5 +2018547 5 +2019724 4 +2020859 5 +2021985 4 +2023157 5 +2024342 4 +2025481 5 +2029142 5 +2030338 4 +2031389 5 +2032455 5 +2033496 4 +2034665 4 +2035853 4 +2036857 5 +2041484 5 +2042668 4 +2043745 5 +2044795 4 +2045971 4 +2046998 5 +2048039 5 +2051921 4 +2053094 4 +2054233 4 +2055353 4 +2056464 5 +2057572 4 +2058755 5 +2059911 5 +2064430 5 +2065490 5 +2066499 4 +2067564 4 +2068710 4 +2069769 5 +2070824 5 +2071936 5 +2076572 5 +2077733 5 +2078748 4 +2079924 5 +2080945 4 +2082026 4 +2083166 4 +2086738 4 +2087841 4 +2088990 4 +2090184 5 +2091335 4 +2092518 5 +2093711 5 +2094739 5 +2099365 5 +2100478 4 +2101562 4 +2102565 4 +2103652 5 +2104755 5 +2105915 5 +2107055 5 +2111319 5 +2112332 4 +2113464 5 +2114517 4 +2115612 5 +2116732 5 +2117759 4 +2122229 4 +2123417 4 +2124450 4 +2125595 5 +2126673 5 +2127798 4 +2128812 4 +2132445 5 +2133483 5 +2134680 4 +2135814 5 +2136878 5 +2138080 5 +2139135 4 +2140266 4 +2143949 5 +2145050 4 +2146081 5 +2147147 5 +2148325 4 +2149505 4 +2150604 5 +2154266 4 +2155326 5 +2156442 4 +2157625 5 +2158724 4 +2159732 5 +2160761 4 +2165362 5 +2166505 5 +2167701 4 +2168829 4 +2169836 5 +2171001 4 +2172083 4 +2173190 4 +2177595 4 +2178711 5 +2179836 4 +2180953 5 +2182150 4 +2183201 4 +2184226 4 +2185310 5 +2189669 4 +2190792 5 +2191847 5 +2192936 5 +2193994 4 +2195087 4 +2196159 4 +2200409 5 +2201552 5 +2202649 4 +2203656 4 +2204828 5 +2205865 4 +2207064 5 +2211341 5 +2212534 5 +2213704 4 +2214715 5 +2215779 5 +2216971 5 +2218058 4 +2219209 4 \ No newline at end of file diff --git a/dist/EEGToolkit-1.0.0-py3-none-any.whl b/dist/EEGToolkit-1.0.0-py3-none-any.whl index 7dead04..3ff841b 100644 Binary files a/dist/EEGToolkit-1.0.0-py3-none-any.whl and b/dist/EEGToolkit-1.0.0-py3-none-any.whl differ diff --git a/dist/EEGToolkit-1.0.0.tar.gz b/dist/EEGToolkit-1.0.0.tar.gz index 036b7a2..9fb6d57 100644 Binary files a/dist/EEGToolkit-1.0.0.tar.gz and b/dist/EEGToolkit-1.0.0.tar.gz differ diff --git a/dist/EEGToolkit-2.0.0-py3-none-any.whl b/dist/EEGToolkit-2.0.0-py3-none-any.whl new file mode 100644 index 0000000..1bdcc60 Binary files /dev/null and b/dist/EEGToolkit-2.0.0-py3-none-any.whl differ diff --git a/dist/EEGToolkit-2.0.0.tar.gz b/dist/EEGToolkit-2.0.0.tar.gz new file mode 100644 index 0000000..91ad7cb Binary files /dev/null and b/dist/EEGToolkit-2.0.0.tar.gz differ diff --git a/docs/EEGToolkit/EEGData.html b/docs/EEGToolkit/EEGData.html index 4904b53..26eaa00 100644 --- a/docs/EEGToolkit/EEGData.html +++ b/docs/EEGToolkit/EEGData.html @@ -26,35 +26,6 @@

Module EEGToolkit.EEGData

This module provides a data class EEGData to work with EEG signal data for event-reaction-time delay experiments. It works with two separate input datafiles, one storing the EEG signal itself as a 1D array, and one describing event metadata as a 2D array, describing both the timepoints and the type of event in two columns.

-

Supported file types are: -- npy -- txt -( space-separated for events datafiles ) -- tsv -- csv -(both , and ; separated ) -

-

Example Usage

-

To use this module for data analysis, only three steps are necessary, -(1st) setup of the EEGData object, (2nd) event data extraction, and (3rd) -data summary (which performs signal comparison).

-
# setting up the EEGData with some datafiles
-eeg = EEGData( eeg_path = "data/eeg.npy", event_path = "data/events.npy", sampling_frequency = 500 )
-
-# extract the events data
-data.extract( start_sec = -0.3 , stop_sec = 1 )
-
-# summarize and pair-wise compare event-signal types.
-data.summary(
-                significance_level = 0.05,
-                x_scale = 1000,
-                y_scale = 10000,
-            )
-
-

CLI

-

This module additionally offers a CLI to directly call the full analysis procedure from the terminal.

-
python3 EEGData.py                     --eeg_path "./data/eeg.npy"                     --event_path "./data/events.npy"                     --sampling_frequency 500                     --p_value 0.05                     --start_sec -0.3                     --stop_sec 1.0                     --x_scale 1000                     --y_scale 10000                     --output "./test_output.pdf"
-
Expand source code @@ -65,51 +36,11 @@

CLI

It works with two separate input datafiles, one storing the EEG signal itself as a 1D array, and one describing event metadata as a 2D array, describing both the timepoints and the type of event in two columns. -Supported file types are: -- `npy` -- `txt` ( space-separated for events datafiles ) -- `tsv` -- `csv` (both `,` and `;` separated ) - -### Example Usage -To use this module for data analysis, only three steps are necessary, -(1st) setup of the `EEGData` object, (2nd) event data extraction, and (3rd) -data summary (which performs signal comparison). - -``` -# setting up the EEGData with some datafiles -eeg = EEGData( eeg_path = "data/eeg.npy", event_path = "data/events.npy", sampling_frequency = 500 ) - -# extract the events data -data.extract( start_sec = -0.3 , stop_sec = 1 ) - -# summarize and pair-wise compare event-signal types. -data.summary( - significance_level = 0.05, - x_scale = 1000, - y_scale = 10000, - ) -``` - -### CLI -This module additionally offers a CLI to directly call the full analysis procedure from the terminal. - -``` -python3 EEGData.py \ - --eeg_path "./data/eeg.npy" \ - --event_path "./data/events.npy" \ - --sampling_frequency 500 \ - --p_value 0.05 \ - --start_sec -0.3 \ - --stop_sec 1.0 \ - --x_scale 1000 \ - --y_scale 10000 \ - --output "./test_output.pdf" -``` - """ -import os +import subprocess +import inspect +import os import argparse import numpy as np import pandas as pd @@ -165,7 +96,10 @@

CLI

self.read( signal_path = signal_path, event_path = event_path ) # now setup the frames for the events - self.n_frames = len(self.signal) + self._n_frames = len(self.signal) + + # this will setup self._events which is a + # dictionary of event identifiers : number of repeated measurements self._set_n_events() # setup a _data argument for the @@ -181,6 +115,22 @@

CLI

self._start_sec = -0.5 self._stop_sec = 1 + # and setup the extracted events in case + # only a subset are being extacted + self._extracted_events = None + + # save a baseline for each event type which will be an + # np.ndarray to store the timepoints (or a subset thereof) + # before the signal onset. The time points will be sampled + # from the extracted timepoints... + self._baseline = None + + # setup a dictionary to store the p-values of pair-wise comparison + # between either two signals or a signal with it's baseline. + # keys will be tuples of signal1, signal2, for baseline comparison + # signal1 = signal2... + self._pvalues = {} + def read( self, signal_path : str = None , event_path : str = None ) -> None: """ Read the provided data files and stores the @@ -235,8 +185,7 @@

CLI

events = self._read_datafile( event_path ) # now save - self.events = events - + self._events_data = events def extract(self, start_sec:float, @@ -282,7 +231,7 @@

CLI

if event_type is None: # get all events - events_to_extract = self.n_events.keys() + events_to_extract = self._events.keys() # extract each type from the loaded data data = [ @@ -291,6 +240,7 @@

CLI

] self._data = data + self._extracted_events = events_to_extract return data # check if there is a provided subset of events to extract @@ -305,30 +255,16 @@

CLI

] self._data = data + self._extracted_events = events_to_extract return data # now the part for extracting only a # single event type data - - # first adjust the start and end to - # match the sampling frequency - start_frame = int( start_sec * self.sampling_frequency ) - stop_frame = int( stop_sec * self.sampling_frequency ) - - # next generate a set of slices for the EEG data around the timepoints for - # the events - firing_slices = [ - slice( event[0]+start_frame, event[0]+stop_frame ) - for event in self.events - if event[1] == event_type - ] - - # now get the actual data of the event - data = [ self.signal[ slice ] for slice in firing_slices ] - data = np.array( data ) - + data = self._extract_window(start_sec, stop_sec, event_type) self._data = data + self._extracted_events = event_type + # store start and stop sec values # for later use in summary() self._start_sec = start_sec @@ -336,6 +272,138 @@

CLI

return data + def baseline( self, size : int or float = None ): + """ + Generates a baseline distribution for EEG Signals, + using random sampling from pre-signal timepoints accross + replicates and events. + + Note + ---- + This requires that events have already been extacted! + + Parameters + ---------- + size : int or float + The number of random samples to draw. If `None` are provided (default) + the entire available pre-signal data is used. If an `integer` is provided + then the final baseline data contains exactly the given number of datapoints. + Alternatively, a `float` `0 < size <= 1` may be provided to specify a fraction + of data to sample from. E.g. `size = 0.2` would incorporate 20% of the available + datapoints into the baseline. + + Returns + ------- + baseline : np.ndarray + An np.ndarray of the randomly drawn samples. + """ + start_sec, stop_sec = self._start_sec, self._stop_sec + + # first get the time period before t=0, beginning at the starting time... + if isinstance( self._data, list ): + random_samples = [ self._extract_window( start_sec, 0, e ) for e in self._extracted_events ] + elif isinstance( self._data, np.ndarray ): + random_samples = [ self._extract_window( start_sec, 0, self._extracted_events ) ] + elif self._data is None: + raise Exception( f"No events data has been extracted yet! Make sure to run extract() before computing a baseline." ) + + # collapse the repeats into a single dataset + random_samples = [ np.reshape( i, i.size ) for i in random_samples ] + + # now if there is a provided size we subsample + if size is not None: + if size <= 1: + random_samples = [ np.random.choice( i, size = size * i.size ) for i in random_samples ] + elif size > 1: + random_samples = [ np.random.choice( i, size = size ) for i in random_samples ] + else: + raise ValueError( f"size needs to be a fraction in [0,1] or an integer > 1 (got size = {size})" ) + + self._baseline = random_samples + + # Alright, we currently have the entire sets of pre-timeframes for the baseline and we + # will use them as they are completely to use for the baseline comparison. + # With the code below we compare a sub-sampled versions thereof. Long story short, + # it works also pretty well with sub-sampled versions as well... + # import statsmodels.api as sm + # from matplotlib import colors + + # fig, ax = plt.subplots( 2,3 ) + # for r in random_samples: + # ax[0,0].plot( r ) + # r1 = r.reshape( r.size ) + # ax[0,1].hist( r1, bins = 50 ) + + # sm.qqplot( r1, ax = ax[0,2], alpha = 0.3, line = "s", color = list(colors.cnames.values())[ int(np.random.randint(low = 0, high = 10, size = 1))] ) + # random_samples = [ np.random.choice( r.reshape(r.size), size = size, replace = False ) for r in random_samples ] + + # for r in random_samples: + # ax[1,0].plot( r ) + # r1 = np.reshape( r, r.size ) + # ax[1,1].hist( r1, bins = 50 ) + + # sm.qqplot( r1, ax = ax[1,2], alpha = 0.3, line = "s", color = list(colors.cnames.values())[ int(np.random.randint(low = 0, high = 10, size = 1))] ) + # # ax[1].hist( np.reshape( random_samples, random_samples.size) ) + # plt.tight_layout() + # plt.show() + + + def pvalues( self, event1 : int, event2 : int = None ): + """ + Gets the p-value np.ndarray for each signal timepoint from a comparison of + either two separate event types or one event with its baseline. + + Parameters + ---------- + event1 : int + The numeric event identifier of the (first) signal to get. + If `None` is provided, the entire dictionary of pvalues is returned. + + event2 : int + The numeric event identifier of the (second) signal from the comparison to get. + If `None` is provided then the first signals comparison to it's baseline will be + returned (if baseline comparison was performed). + + Returns + ------- + pvalues : np.ndarray or dict + An np.ndarray of p-values from a given comparison. + """ + + if event1 is None: + return self._pvalues + + if event2 is None: + key = (event1, event1) + else: + key = (event1, event2) + pvalues = self._pvalues.get( key, None ) + return pvalues + + @property + def events( self ): + """ + Returns + ------- + list + A list of all different event types from from the loaded metadata. + """ + return list( self._events.keys() ) + + @property + def timeframe( self ): + """ + Returns + ------- + tuple + The used timeframe for event data extraction. + This consists of the pre-trigger and post-trigger + time offsets in seconds. + """ + return ( self._start_sec, self._stop_sec ) + + + def summary(self, x_scale:float, y_scale:float, @@ -374,37 +442,48 @@

CLI

""" # extract the event data if not yet done already + start_sec = kwargs.pop( "start_sec", self._start_sec ) + stop_sec = kwargs.pop( "stop_sec", self._stop_sec ) if self._data is None: - start_sec = kwargs.pop( "start_sec", self._start_sec ) - stop_sec = kwargs.pop( "stop_sec", self._stop_sec ) self.extract( start_sec = start_sec, stop_sec = stop_sec, **kwargs ) + self.baseline() data = list( self._data ) - signals = list(self.n_events.keys()) + signals = list(self._events.keys()) n = len(data) - start_sec, stop_sec = self._start_sec, self._stop_sec - # generate a new figure - fig, ax = plt.subplots(n,n) + figsize = kwargs.pop( "figsize", ( 3*n,2*n ) ) + fig, ax = plt.subplots(n,n, figsize = figsize ) + # setup a baseline reference, either with the computed + # baselines or None ... + baseline = self._baseline if self._baseline is not None else [ None for i in range(n) ] + # now first plot the individual signals # on their own on diagonal plots for i in range(n): # only the last subplot should make a legend make_legend = i == n-1 - plot_signal( + p = plot_signal( data[i], self.sampling_frequency, start_sec, stop_sec, x_scale, y_scale, + baseline = baseline[i], make_legend = make_legend, + significance_level = significance_level, ax = ax[i,i] ) ax[i,i].set_title(f"Signal {signals[i]}") + # if we got a baseline to compare to we also want to + # store the resulting p-values + if p is not None: + self._pvalues[ (i,i) ] = p + # hide all "left-over" subplots from the layout # i.e. hide the upper-right half of the figure... for a in ax[ i, i+1: ]: @@ -418,7 +497,7 @@

CLI

# only the last plot shall make a legend make_legend = i == n-1 and j == i-1 - difference_plot( + p = difference_plot( data[i], data[j], self.sampling_frequency, @@ -430,6 +509,10 @@

CLI

) ax[i,j].set_title(f"Signals: {signals[j]} vs {signals[i]}") + # we also want to store the resulting p-values of the + # signal comparison + self._pvalues[ ( signals[j],signals[i] ) ] = p + fig.tight_layout() if output is None: @@ -437,14 +520,47 @@

CLI

return fig plt.savefig(output, bbox_inches = "tight" ) + + def _extract_window(self, start_sec, stop_sec, event_type): + """ + Extracts a set of time-frame windows from the data + and returns them as a numpy ndarray. + """ + + # first adjust the start and end to + # match the sampling frequency + start_frame, stop_frame = self._adjust_timesteps(start_sec, stop_sec) + + # next generate a set of slices for the EEG data around the timepoints for + # the events + firing_slices = [ + slice( event[0]+start_frame, event[0]+stop_frame ) + for event in self._events_data + if event[1] == event_type + ] + + # now get the actual data of the event + data = [ self.signal[ slice ] for slice in firing_slices ] + data = np.array( data ) + return data + + def _adjust_timesteps(self, start_sec, stop_sec): + """ + Adjusts time steps / time points with the used recording frequency, + to match the indices within the data. + """ + start_frame = int( start_sec * self.sampling_frequency ) + stop_frame = int( stop_sec * self.sampling_frequency ) + return start_frame,stop_frame + def _set_n_events(self) -> None: """ Sets up a dictionary of the different event types found in the events data. """ - event_types = {event[1] for event in self.events} - self.n_events = {event_type: len([event for event in self.events if event[1] == event_type]) for event_type in event_types} + event_types = {event[1] for event in self._events_data} + self._events = {event_type: len([event for event in self._events_data if event[1] == event_type]) for event_type in event_types} def _check_sanity(self, signal_path, event_path, sampling_frequency): """ @@ -546,16 +662,34 @@

CLI

--output "./test.png" """ - parser = argparse.ArgumentParser(prefix_chars='-') + descr1 = """ + +----------------------------------------------------- +▒█▀▀▀ ▒█▀▀▀ ▒█▀▀█ ▀▀█▀▀ █▀▀█ █▀▀█ █░░ ▒█░▄▀ ░▀░ ▀▀█▀▀ +▒█▀▀▀ ▒█▀▀▀ ▒█░▄▄ ░▒█░░ █░░█ █░░█ █░░ ▒█▀▄░ ▀█▀ ░░█░░ +▒█▄▄▄ ▒█▄▄▄ ▒█▄▄█ ░▒█░░ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▒█░▒█ ▀▀▀ ░░▀░░ +----------------------------------------------------- + +This script takes in two data files of EEG signal data and accompanying event-trigger metadata. It performs intra- and inter-signal type comparisons using pair-wise T-Tests over the time-series, highlighting significantly different stretches and producing a summary figure. + """ + descr2 = f""" + +Input Data +---------- +Accepted input file types are {supported_filetypes}. The EEG-signal datafile must specify a 1D array of measurements, while the trigger metadata file must specify a 2D array (2 columns) of trigger time points and event classifier labels (numerically encoded). + """ + + parser = argparse.ArgumentParser( prefix_chars = "-", + formatter_class=argparse.RawDescriptionHelpFormatter,description = descr1, epilog = descr2 ) parser.add_argument( "--eeg_path", "--eeg", - type=str, required=True, + type=str, help = f"A file containing EEG signal data. Supported filetypes are {supported_filetypes}" ) parser.add_argument( "--event_path", "--event", - type=str, required=True, - help = "A file containing event metadata for the signal file. Supported filetypes are {supported_filetypes}" + type=str, + help = f"A file containing event metadata for the signal file. Supported filetypes are {supported_filetypes}" ) parser.add_argument( "--output", "-o", @@ -564,7 +698,7 @@

CLI

) parser.add_argument( "--sampling_frequency", "--freq", "-f", - type=float, required=True, + type=float, help = "The frequency at which the EEG signal data was recorded (in Hertz)." ) parser.add_argument( @@ -574,14 +708,20 @@

CLI

) parser.add_argument( "--start_sec", "--start", "-s", - type=float, required=True, + type=float, help = "The upstream time-padding for event extraction (in seconds)." ) parser.add_argument( "--stop_sec", "--stop", "-e", - type=float, required=True, + type=float, help = "The downstream time-padding for event extraction (in seconds)." ) + + parser.add_argument( + "--baseline", "-b", + type=bool, default = True, + help = "Perform baseline comparison for each event type using the same significance threshold as used for inter-signal comparisons. Will be performed by default." + ) parser.add_argument( "--x_scale", "-x", type=float, default = 1000, @@ -592,24 +732,63 @@

CLI

type=float, default = 1000, help = "A scaling factor for the signal-scale (y-values) from volts to some other unit. Default is 1000 (= millivolts)." ) + + parser.add_argument( + "--viewer", "-i", + action="store_true", + default = False, + help = "Open the EEGToolKit Viewer GUI in a web browser." + ) args = parser.parse_args() - # the main program (reading datafiles, extracting, and summarizing) - data = EEGData(args.eeg_path, args.event_path, args.sampling_frequency) - data.extract( args.start_sec, args.stop_sec ) - data.summary( - significance_level = args.p_value, - x_scale = args.x_scale, - y_scale = args.y_scale, - output = args.output - ) - - if args.output is not None: - print( f"Output saved successfully to: '{args.output}'" ) + # if the viewer is being called then we want to just open the + # viewer and nothing else + if args.viewer: + # first we need to get the relative location of the main. + # py file within the package. + directory = os.path.dirname( + inspect.getfile( plot_signal ) + ) + main_file = f"{directory}/main.py" + + # then we call the web interface + print( "Starting the \033[94mEEGToolKit \033[96mViewer" ) + subprocess.run( f"streamlit run {main_file}", shell = True ) + + else: + + # the main program (reading datafiles, extracting, and summarizing) + data = EEGData(args.eeg_path, args.event_path, args.sampling_frequency) + data.extract( args.start_sec, args.stop_sec ) + if args.baseline: + data.baseline() + data.summary( + significance_level = args.p_value, + x_scale = args.x_scale, + y_scale = args.y_scale, + output = args.output + ) + + if args.output is not None: + print( f"Output saved successfully to: '{args.output}'" ) if __name__ == "__main__": - main() + + test_mode = False + if not test_mode: + main() + else: + print( "Running in Test Mode" ) + + eeg = "./data/eeg.npy" + events = "./data/events.npy" + + e = EEGData( eeg, events, 500 ) + e.extract( -0.3, 1 ) + e.baseline() + e.summary( 1000, 1000, output = "./test.pdf" ) + plt.show()
@@ -657,16 +836,34 @@

Example Usage

--output "./test.png" """ - parser = argparse.ArgumentParser(prefix_chars='-') + descr1 = """ + +----------------------------------------------------- +▒█▀▀▀ ▒█▀▀▀ ▒█▀▀█ ▀▀█▀▀ █▀▀█ █▀▀█ █░░ ▒█░▄▀ ░▀░ ▀▀█▀▀ +▒█▀▀▀ ▒█▀▀▀ ▒█░▄▄ ░▒█░░ █░░█ █░░█ █░░ ▒█▀▄░ ▀█▀ ░░█░░ +▒█▄▄▄ ▒█▄▄▄ ▒█▄▄█ ░▒█░░ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▒█░▒█ ▀▀▀ ░░▀░░ +----------------------------------------------------- + +This script takes in two data files of EEG signal data and accompanying event-trigger metadata. It performs intra- and inter-signal type comparisons using pair-wise T-Tests over the time-series, highlighting significantly different stretches and producing a summary figure. + """ + descr2 = f""" + +Input Data +---------- +Accepted input file types are {supported_filetypes}. The EEG-signal datafile must specify a 1D array of measurements, while the trigger metadata file must specify a 2D array (2 columns) of trigger time points and event classifier labels (numerically encoded). + """ + + parser = argparse.ArgumentParser( prefix_chars = "-", + formatter_class=argparse.RawDescriptionHelpFormatter,description = descr1, epilog = descr2 ) parser.add_argument( "--eeg_path", "--eeg", - type=str, required=True, + type=str, help = f"A file containing EEG signal data. Supported filetypes are {supported_filetypes}" ) parser.add_argument( "--event_path", "--event", - type=str, required=True, - help = "A file containing event metadata for the signal file. Supported filetypes are {supported_filetypes}" + type=str, + help = f"A file containing event metadata for the signal file. Supported filetypes are {supported_filetypes}" ) parser.add_argument( "--output", "-o", @@ -675,7 +872,7 @@

Example Usage

) parser.add_argument( "--sampling_frequency", "--freq", "-f", - type=float, required=True, + type=float, help = "The frequency at which the EEG signal data was recorded (in Hertz)." ) parser.add_argument( @@ -685,14 +882,20 @@

Example Usage

) parser.add_argument( "--start_sec", "--start", "-s", - type=float, required=True, + type=float, help = "The upstream time-padding for event extraction (in seconds)." ) parser.add_argument( "--stop_sec", "--stop", "-e", - type=float, required=True, + type=float, help = "The downstream time-padding for event extraction (in seconds)." ) + + parser.add_argument( + "--baseline", "-b", + type=bool, default = True, + help = "Perform baseline comparison for each event type using the same significance threshold as used for inter-signal comparisons. Will be performed by default." + ) parser.add_argument( "--x_scale", "-x", type=float, default = 1000, @@ -703,21 +906,46 @@

Example Usage

type=float, default = 1000, help = "A scaling factor for the signal-scale (y-values) from volts to some other unit. Default is 1000 (= millivolts)." ) + + parser.add_argument( + "--viewer", "-i", + action="store_true", + default = False, + help = "Open the EEGToolKit Viewer GUI in a web browser." + ) args = parser.parse_args() - # the main program (reading datafiles, extracting, and summarizing) - data = EEGData(args.eeg_path, args.event_path, args.sampling_frequency) - data.extract( args.start_sec, args.stop_sec ) - data.summary( - significance_level = args.p_value, - x_scale = args.x_scale, - y_scale = args.y_scale, - output = args.output - ) - - if args.output is not None: - print( f"Output saved successfully to: '{args.output}'" ) + # if the viewer is being called then we want to just open the + # viewer and nothing else + if args.viewer: + # first we need to get the relative location of the main. + # py file within the package. + directory = os.path.dirname( + inspect.getfile( plot_signal ) + ) + main_file = f"{directory}/main.py" + + # then we call the web interface + print( "Starting the \033[94mEEGToolKit \033[96mViewer" ) + subprocess.run( f"streamlit run {main_file}", shell = True ) + + else: + + # the main program (reading datafiles, extracting, and summarizing) + data = EEGData(args.eeg_path, args.event_path, args.sampling_frequency) + data.extract( args.start_sec, args.stop_sec ) + if args.baseline: + data.baseline() + data.summary( + significance_level = args.p_value, + x_scale = args.x_scale, + y_scale = args.y_scale, + output = args.output + ) + + if args.output is not None: + print( f"Output saved successfully to: '{args.output}'" ) @@ -807,7 +1035,10 @@

Parameters

self.read( signal_path = signal_path, event_path = event_path ) # now setup the frames for the events - self.n_frames = len(self.signal) + self._n_frames = len(self.signal) + + # this will setup self._events which is a + # dictionary of event identifiers : number of repeated measurements self._set_n_events() # setup a _data argument for the @@ -823,6 +1054,22 @@

Parameters

self._start_sec = -0.5 self._stop_sec = 1 + # and setup the extracted events in case + # only a subset are being extacted + self._extracted_events = None + + # save a baseline for each event type which will be an + # np.ndarray to store the timepoints (or a subset thereof) + # before the signal onset. The time points will be sampled + # from the extracted timepoints... + self._baseline = None + + # setup a dictionary to store the p-values of pair-wise comparison + # between either two signals or a signal with it's baseline. + # keys will be tuples of signal1, signal2, for baseline comparison + # signal1 = signal2... + self._pvalues = {} + def read( self, signal_path : str = None , event_path : str = None ) -> None: """ Read the provided data files and stores the @@ -877,8 +1124,7 @@

Parameters

events = self._read_datafile( event_path ) # now save - self.events = events - + self._events_data = events def extract(self, start_sec:float, @@ -924,7 +1170,7 @@

Parameters

if event_type is None: # get all events - events_to_extract = self.n_events.keys() + events_to_extract = self._events.keys() # extract each type from the loaded data data = [ @@ -933,6 +1179,7 @@

Parameters

] self._data = data + self._extracted_events = events_to_extract return data # check if there is a provided subset of events to extract @@ -947,30 +1194,16 @@

Parameters

] self._data = data + self._extracted_events = events_to_extract return data # now the part for extracting only a # single event type data - - # first adjust the start and end to - # match the sampling frequency - start_frame = int( start_sec * self.sampling_frequency ) - stop_frame = int( stop_sec * self.sampling_frequency ) - - # next generate a set of slices for the EEG data around the timepoints for - # the events - firing_slices = [ - slice( event[0]+start_frame, event[0]+stop_frame ) - for event in self.events - if event[1] == event_type - ] - - # now get the actual data of the event - data = [ self.signal[ slice ] for slice in firing_slices ] - data = np.array( data ) - + data = self._extract_window(start_sec, stop_sec, event_type) self._data = data + self._extracted_events = event_type + # store start and stop sec values # for later use in summary() self._start_sec = start_sec @@ -978,6 +1211,138 @@

Parameters

return data + def baseline( self, size : int or float = None ): + """ + Generates a baseline distribution for EEG Signals, + using random sampling from pre-signal timepoints accross + replicates and events. + + Note + ---- + This requires that events have already been extacted! + + Parameters + ---------- + size : int or float + The number of random samples to draw. If `None` are provided (default) + the entire available pre-signal data is used. If an `integer` is provided + then the final baseline data contains exactly the given number of datapoints. + Alternatively, a `float` `0 < size <= 1` may be provided to specify a fraction + of data to sample from. E.g. `size = 0.2` would incorporate 20% of the available + datapoints into the baseline. + + Returns + ------- + baseline : np.ndarray + An np.ndarray of the randomly drawn samples. + """ + start_sec, stop_sec = self._start_sec, self._stop_sec + + # first get the time period before t=0, beginning at the starting time... + if isinstance( self._data, list ): + random_samples = [ self._extract_window( start_sec, 0, e ) for e in self._extracted_events ] + elif isinstance( self._data, np.ndarray ): + random_samples = [ self._extract_window( start_sec, 0, self._extracted_events ) ] + elif self._data is None: + raise Exception( f"No events data has been extracted yet! Make sure to run extract() before computing a baseline." ) + + # collapse the repeats into a single dataset + random_samples = [ np.reshape( i, i.size ) for i in random_samples ] + + # now if there is a provided size we subsample + if size is not None: + if size <= 1: + random_samples = [ np.random.choice( i, size = size * i.size ) for i in random_samples ] + elif size > 1: + random_samples = [ np.random.choice( i, size = size ) for i in random_samples ] + else: + raise ValueError( f"size needs to be a fraction in [0,1] or an integer > 1 (got size = {size})" ) + + self._baseline = random_samples + + # Alright, we currently have the entire sets of pre-timeframes for the baseline and we + # will use them as they are completely to use for the baseline comparison. + # With the code below we compare a sub-sampled versions thereof. Long story short, + # it works also pretty well with sub-sampled versions as well... + # import statsmodels.api as sm + # from matplotlib import colors + + # fig, ax = plt.subplots( 2,3 ) + # for r in random_samples: + # ax[0,0].plot( r ) + # r1 = r.reshape( r.size ) + # ax[0,1].hist( r1, bins = 50 ) + + # sm.qqplot( r1, ax = ax[0,2], alpha = 0.3, line = "s", color = list(colors.cnames.values())[ int(np.random.randint(low = 0, high = 10, size = 1))] ) + # random_samples = [ np.random.choice( r.reshape(r.size), size = size, replace = False ) for r in random_samples ] + + # for r in random_samples: + # ax[1,0].plot( r ) + # r1 = np.reshape( r, r.size ) + # ax[1,1].hist( r1, bins = 50 ) + + # sm.qqplot( r1, ax = ax[1,2], alpha = 0.3, line = "s", color = list(colors.cnames.values())[ int(np.random.randint(low = 0, high = 10, size = 1))] ) + # # ax[1].hist( np.reshape( random_samples, random_samples.size) ) + # plt.tight_layout() + # plt.show() + + + def pvalues( self, event1 : int, event2 : int = None ): + """ + Gets the p-value np.ndarray for each signal timepoint from a comparison of + either two separate event types or one event with its baseline. + + Parameters + ---------- + event1 : int + The numeric event identifier of the (first) signal to get. + If `None` is provided, the entire dictionary of pvalues is returned. + + event2 : int + The numeric event identifier of the (second) signal from the comparison to get. + If `None` is provided then the first signals comparison to it's baseline will be + returned (if baseline comparison was performed). + + Returns + ------- + pvalues : np.ndarray or dict + An np.ndarray of p-values from a given comparison. + """ + + if event1 is None: + return self._pvalues + + if event2 is None: + key = (event1, event1) + else: + key = (event1, event2) + pvalues = self._pvalues.get( key, None ) + return pvalues + + @property + def events( self ): + """ + Returns + ------- + list + A list of all different event types from from the loaded metadata. + """ + return list( self._events.keys() ) + + @property + def timeframe( self ): + """ + Returns + ------- + tuple + The used timeframe for event data extraction. + This consists of the pre-trigger and post-trigger + time offsets in seconds. + """ + return ( self._start_sec, self._stop_sec ) + + + def summary(self, x_scale:float, y_scale:float, @@ -1016,37 +1381,48 @@

Parameters

""" # extract the event data if not yet done already + start_sec = kwargs.pop( "start_sec", self._start_sec ) + stop_sec = kwargs.pop( "stop_sec", self._stop_sec ) if self._data is None: - start_sec = kwargs.pop( "start_sec", self._start_sec ) - stop_sec = kwargs.pop( "stop_sec", self._stop_sec ) self.extract( start_sec = start_sec, stop_sec = stop_sec, **kwargs ) + self.baseline() data = list( self._data ) - signals = list(self.n_events.keys()) + signals = list(self._events.keys()) n = len(data) - start_sec, stop_sec = self._start_sec, self._stop_sec - # generate a new figure - fig, ax = plt.subplots(n,n) + figsize = kwargs.pop( "figsize", ( 3*n,2*n ) ) + fig, ax = plt.subplots(n,n, figsize = figsize ) + # setup a baseline reference, either with the computed + # baselines or None ... + baseline = self._baseline if self._baseline is not None else [ None for i in range(n) ] + # now first plot the individual signals # on their own on diagonal plots for i in range(n): # only the last subplot should make a legend make_legend = i == n-1 - plot_signal( + p = plot_signal( data[i], self.sampling_frequency, start_sec, stop_sec, x_scale, y_scale, + baseline = baseline[i], make_legend = make_legend, + significance_level = significance_level, ax = ax[i,i] ) ax[i,i].set_title(f"Signal {signals[i]}") + # if we got a baseline to compare to we also want to + # store the resulting p-values + if p is not None: + self._pvalues[ (i,i) ] = p + # hide all "left-over" subplots from the layout # i.e. hide the upper-right half of the figure... for a in ax[ i, i+1: ]: @@ -1060,7 +1436,7 @@

Parameters

# only the last plot shall make a legend make_legend = i == n-1 and j == i-1 - difference_plot( + p = difference_plot( data[i], data[j], self.sampling_frequency, @@ -1072,6 +1448,10 @@

Parameters

) ax[i,j].set_title(f"Signals: {signals[j]} vs {signals[i]}") + # we also want to store the resulting p-values of the + # signal comparison + self._pvalues[ ( signals[j],signals[i] ) ] = p + fig.tight_layout() if output is None: @@ -1079,14 +1459,47 @@

Parameters

return fig plt.savefig(output, bbox_inches = "tight" ) + + def _extract_window(self, start_sec, stop_sec, event_type): + """ + Extracts a set of time-frame windows from the data + and returns them as a numpy ndarray. + """ + + # first adjust the start and end to + # match the sampling frequency + start_frame, stop_frame = self._adjust_timesteps(start_sec, stop_sec) + + # next generate a set of slices for the EEG data around the timepoints for + # the events + firing_slices = [ + slice( event[0]+start_frame, event[0]+stop_frame ) + for event in self._events_data + if event[1] == event_type + ] + + # now get the actual data of the event + data = [ self.signal[ slice ] for slice in firing_slices ] + data = np.array( data ) + return data + + def _adjust_timesteps(self, start_sec, stop_sec): + """ + Adjusts time steps / time points with the used recording frequency, + to match the indices within the data. + """ + start_frame = int( start_sec * self.sampling_frequency ) + stop_frame = int( stop_sec * self.sampling_frequency ) + return start_frame,stop_frame + def _set_n_events(self) -> None: """ Sets up a dictionary of the different event types found in the events data. """ - event_types = {event[1] for event in self.events} - self.n_events = {event_type: len([event for event in self.events if event[1] == event_type]) for event_type in event_types} + event_types = {event[1] for event in self._events_data} + self._events = {event_type: len([event for event in self._events_data if event[1] == event_type]) for event_type in event_types} def _check_sanity(self, signal_path, event_path, sampling_frequency): """ @@ -1170,8 +1583,166 @@

Parameters

return delimiter +

Subclasses

+ +

Instance variables

+
+
var events
+
+

Returns

+

list +A list of all different event types from from the loaded metadata.

+
+ +Expand source code + +
@property
+def events( self ):
+    """
+    Returns 
+    -------
+    list
+        A list of all different event types from from the loaded metadata.
+    """
+    return list( self._events.keys() )
+
+
+
var timeframe
+
+

Returns

+
+
tuple +
+
The used timeframe for event data extraction. +This consists of the pre-trigger and post-trigger +time offsets in seconds.
+
+
+ +Expand source code + +
@property
+def timeframe( self ):
+    """
+    Returns
+    -------
+    tuple   
+        The used timeframe for event data extraction.
+        This consists of the pre-trigger and post-trigger
+        time offsets in seconds.
+    """
+    return ( self._start_sec, self._stop_sec )
+
+
+

Methods

+
+def baseline(self, size: int = None) +
+
+

Generates a baseline distribution for EEG Signals, +using random sampling from pre-signal timepoints accross +replicates and events.

+

Note

+

This requires that events have already been extacted!

+

Parameters

+
+
size : int or float
+
The number of random samples to draw. If None are provided (default) +the entire available pre-signal data is used. If an integer is provided +then the final baseline data contains exactly the given number of datapoints. +Alternatively, a float 0 < size <= 1 may be provided to specify a fraction +of data to sample from. E.g. size = 0.2 would incorporate 20% of the available +datapoints into the baseline.
+
+

Returns

+
+
baseline : np.ndarray
+
An np.ndarray of the randomly drawn samples.
+
+
+ +Expand source code + +
def baseline( self, size : int or float = None ):
+    """
+    Generates a baseline distribution for EEG Signals,
+    using random sampling from pre-signal timepoints accross 
+    replicates and events.
+
+    Note
+    ----
+    This requires that events have already been extacted!
+
+    Parameters
+    ----------
+    size : int or float
+        The number of random samples to draw. If `None` are provided (default)
+        the entire available pre-signal data is used. If an `integer` is provided
+        then the final baseline data contains exactly the given number of datapoints.
+        Alternatively, a `float` `0 < size <= 1` may be provided to specify a fraction
+        of data to sample from. E.g. `size = 0.2` would incorporate 20% of the available
+        datapoints into the baseline.
+    
+    Returns
+    -------
+    baseline : np.ndarray
+        An np.ndarray of the randomly drawn samples.
+    """
+    start_sec, stop_sec = self._start_sec, self._stop_sec
+
+    # first get the time period before t=0, beginning at the starting time...
+    if isinstance( self._data, list ):
+        random_samples = [ self._extract_window( start_sec, 0, e ) for e in self._extracted_events ]
+    elif isinstance( self._data, np.ndarray ):
+        random_samples = [ self._extract_window( start_sec, 0, self._extracted_events ) ]        
+    elif self._data is None:
+        raise Exception( f"No events data has been extracted yet! Make sure to run extract() before computing a baseline." )
+
+    # collapse the repeats into a single dataset
+    random_samples = [ np.reshape( i, i.size ) for i in random_samples ] 
+
+    # now if there is a provided size we subsample
+    if size is not None: 
+        if size <= 1:
+            random_samples = [ np.random.choice( i, size = size * i.size ) for i in random_samples ]
+        elif size > 1:
+            random_samples = [ np.random.choice( i, size = size ) for i in random_samples ]
+        else:
+            raise ValueError( f"size needs to be a fraction in [0,1] or an integer > 1 (got size = {size})" )
+
+    self._baseline = random_samples
+
+    # Alright, we currently have the entire sets of pre-timeframes for the baseline and we
+    # will use them as they are completely to use for the baseline comparison. 
+    # With the code below we compare a sub-sampled versions thereof. Long story short,
+    # it works also pretty well with sub-sampled versions as well...
+    # import statsmodels.api as sm
+    # from matplotlib import colors
+
+    # fig, ax = plt.subplots( 2,3 ) 
+    # for r in random_samples:
+    #     ax[0,0].plot( r )
+    #     r1 = r.reshape( r.size )
+    #     ax[0,1].hist( r1, bins = 50 )
+
+    #     sm.qqplot( r1, ax = ax[0,2], alpha = 0.3, line = "s", color = list(colors.cnames.values())[ int(np.random.randint(low = 0, high = 10, size = 1))]  )
+    # random_samples = [ np.random.choice( r.reshape(r.size), size = size, replace = False ) for r in random_samples ]
+    
+    # for r in random_samples:
+    #     ax[1,0].plot( r )
+    #     r1 = np.reshape( r, r.size )
+    #     ax[1,1].hist( r1, bins = 50 )
+        
+    #     sm.qqplot( r1, ax = ax[1,2], alpha = 0.3, line = "s", color =  list(colors.cnames.values())[ int(np.random.randint(low = 0, high = 10, size = 1))] )
+    # # ax[1].hist( np.reshape( random_samples, random_samples.size)  )
+    # plt.tight_layout()
+    # plt.show()
+
+
def extract(self, start_sec: float, stop_sec: float, event_type: int = None) ‑> numpy.ndarray
@@ -1253,7 +1824,7 @@

Returns

if event_type is None: # get all events - events_to_extract = self.n_events.keys() + events_to_extract = self._events.keys() # extract each type from the loaded data data = [ @@ -1262,6 +1833,7 @@

Returns

] self._data = data + self._extracted_events = events_to_extract return data # check if there is a provided subset of events to extract @@ -1276,30 +1848,16 @@

Returns

] self._data = data + self._extracted_events = events_to_extract return data # now the part for extracting only a # single event type data - - # first adjust the start and end to - # match the sampling frequency - start_frame = int( start_sec * self.sampling_frequency ) - stop_frame = int( stop_sec * self.sampling_frequency ) - - # next generate a set of slices for the EEG data around the timepoints for - # the events - firing_slices = [ - slice( event[0]+start_frame, event[0]+stop_frame ) - for event in self.events - if event[1] == event_type - ] - - # now get the actual data of the event - data = [ self.signal[ slice ] for slice in firing_slices ] - data = np.array( data ) - + data = self._extract_window(start_sec, stop_sec, event_type) self._data = data + self._extracted_events = event_type + # store start and stop sec values # for later use in summary() self._start_sec = start_sec @@ -1308,6 +1866,64 @@

Returns

return data +
+def pvalues(self, event1: int, event2: int = None) +
+
+

Gets the p-value np.ndarray for each signal timepoint from a comparison of +either two separate event types or one event with its baseline.

+

Parameters

+
+
event1 : int
+
The numeric event identifier of the (first) signal to get. +If None is provided, the entire dictionary of pvalues is returned.
+
event2 : int
+
The numeric event identifier of the (second) signal from the comparison to get. +If None is provided then the first signals comparison to it's baseline will be +returned (if baseline comparison was performed).
+
+

Returns

+
+
pvalues : np.ndarray or dict
+
An np.ndarray of p-values from a given comparison.
+
+
+ +Expand source code + +
def pvalues( self, event1 : int, event2 : int = None ):
+    """
+    Gets the p-value np.ndarray for each signal timepoint from a comparison of 
+    either two separate event types or one event with its baseline. 
+
+    Parameters
+    ----------
+    event1 : int
+        The numeric event identifier of the (first) signal to get.
+        If `None` is provided, the entire dictionary of pvalues is returned.
+
+    event2 : int
+        The numeric event identifier of the (second) signal from the comparison to get.
+        If `None` is provided then the first signals comparison to it's baseline will be 
+        returned (if baseline comparison was performed).
+    
+    Returns
+    -------
+    pvalues : np.ndarray or dict
+        An np.ndarray of p-values from a given comparison.
+    """
+
+    if event1 is None: 
+        return self._pvalues
+
+    if event2 is None:
+        key = (event1, event1)
+    else: 
+        key = (event1, event2)
+    pvalues = self._pvalues.get( key, None )
+    return pvalues 
+
+
def read(self, signal_path: str = None, event_path: str = None) ‑> None
@@ -1396,7 +2012,7 @@

Parameters

events = self._read_datafile( event_path ) # now save - self.events = events + self._events_data = events
@@ -1468,20 +2084,24 @@

Parameters

""" # extract the event data if not yet done already + start_sec = kwargs.pop( "start_sec", self._start_sec ) + stop_sec = kwargs.pop( "stop_sec", self._stop_sec ) if self._data is None: - start_sec = kwargs.pop( "start_sec", self._start_sec ) - stop_sec = kwargs.pop( "stop_sec", self._stop_sec ) self.extract( start_sec = start_sec, stop_sec = stop_sec, **kwargs ) + self.baseline() data = list( self._data ) - signals = list(self.n_events.keys()) + signals = list(self._events.keys()) n = len(data) - start_sec, stop_sec = self._start_sec, self._stop_sec - # generate a new figure - fig, ax = plt.subplots(n,n) + figsize = kwargs.pop( "figsize", ( 3*n,2*n ) ) + fig, ax = plt.subplots(n,n, figsize = figsize ) + + # setup a baseline reference, either with the computed + # baselines or None ... + baseline = self._baseline if self._baseline is not None else [ None for i in range(n) ] # now first plot the individual signals # on their own on diagonal plots @@ -1489,16 +2109,23 @@

Parameters

# only the last subplot should make a legend make_legend = i == n-1 - plot_signal( + p = plot_signal( data[i], self.sampling_frequency, start_sec, stop_sec, x_scale, y_scale, + baseline = baseline[i], make_legend = make_legend, + significance_level = significance_level, ax = ax[i,i] ) ax[i,i].set_title(f"Signal {signals[i]}") + # if we got a baseline to compare to we also want to + # store the resulting p-values + if p is not None: + self._pvalues[ (i,i) ] = p + # hide all "left-over" subplots from the layout # i.e. hide the upper-right half of the figure... for a in ax[ i, i+1: ]: @@ -1512,7 +2139,7 @@

Parameters

# only the last plot shall make a legend make_legend = i == n-1 and j == i-1 - difference_plot( + p = difference_plot( data[i], data[j], self.sampling_frequency, @@ -1524,6 +2151,10 @@

Parameters

) ax[i,j].set_title(f"Signals: {signals[j]} vs {signals[i]}") + # we also want to store the resulting p-values of the + # signal comparison + self._pvalues[ ( signals[j],signals[i] ) ] = p + fig.tight_layout() if output is None: @@ -1540,10 +2171,7 @@

Parameters

@@ -559,6 +662,13 @@

Parameters

E.g. y_scale = 1000 to adjust the signal-scale to millivolts.
make_legend : bool
Generates a legend for the plot.
+ +

Returns

+
+
pvalues : np.ndarray +
+
The p-value from pair-wise T-Test comparison between the +two signals at each signal position (timepoint).
@@ -598,7 +708,7 @@

Parameters

start_sec : float The initial / low bound of the time-scale in seconds. - + stop_sec : float The final / upper bound of the time-scale in seconds. @@ -612,6 +722,12 @@

Parameters

make_legend : bool Generates a legend for the plot. + + Returns + ----- + pvalues : np.ndarray + The p-value from pair-wise T-Test comparison between the + two signals at each signal position (timepoint). """ # generate a new figure if no ax is given @@ -620,22 +736,24 @@

Parameters

else: fig = None - _difference_plot_(ax, - extracted_EEGData_1, - extracted_EEGData_2, - significance_level, - sampling_frequency, - start_sec, - stop_sec, - x_scale, - y_scale, - make_legend - ) - + pvalues = _difference_plot_(ax, + extracted_EEGData_1, + extracted_EEGData_2, + significance_level, + sampling_frequency, + start_sec, + stop_sec, + x_scale, + y_scale, + make_legend + ) + # we show the figure only if no ax was provided # and thus no "bigger" figure is assembled elsewhere... if fig is not None: - plt.show() + plt.show() + + return pvalues
@@ -680,7 +798,7 @@

Returns

-def plot_signal(extracted_EEGData: numpy.ndarray, sampling_frequency: float, start_sec: float, stop_sec: float, x_scale=1000, y_scale=1000, make_legend=False, ax=None) ‑> None +def plot_signal(extracted_EEGData: numpy.ndarray, sampling_frequency: float, start_sec: float, stop_sec: float, x_scale=1000, y_scale=1000, baseline: numpy.ndarray = None, significance_level: float = 0.05, make_legend=False, ax=None) ‑> None

Visualises a single EGG signal dataset from an m x n numpy ndarray @@ -703,8 +821,22 @@

Parameters

y_scale : float
A scaling factor for the data's y-value range. E.g. y_scale = 1000 to adjust the signal-scale to millivolts.
+
baseline : np.ndarray
+
An array of baseline data to compare the signal to using a pair-wise t-test. +This will introduce shaded boxes for regions that show significant differences +between the signal and the baseline.
+
significance_level : float
+
The significance threshold for which to accept a signal difference +as significant. Default is 0.05.
make_legend : bool
Generates a legend for the plot.
+ +

Returns

+
+
pvalues : np.ndarray +
+
The p-value from pair-wise T-Test comparison between the +signal and the baseline (if provided) at each signal position (timepoint).
@@ -717,6 +849,8 @@

Parameters

stop_sec : float, x_scale = 10**3, y_scale = 10**3, + baseline : np.ndarray = None, + significance_level:float = 0.05, make_legend = False, ax = None ) -> None: """ @@ -747,9 +881,23 @@

Parameters

A scaling factor for the data's y-value range. E.g. `y_scale = 1000` to adjust the signal-scale to millivolts. + baseline : np.ndarray + An array of baseline data to compare the signal to using a pair-wise t-test. + This will introduce shaded boxes for regions that show significant differences + between the signal and the baseline. + + significance_level : float + The significance threshold for which to accept a signal difference + as significant. Default is `0.05`. + make_legend : bool Generates a legend for the plot. + Returns + ----- + pvalues : np.ndarray + The p-value from pair-wise T-Test comparison between the + signal and the baseline (if provided) at each signal position (timepoint). """ # generate a new figure if no ax is specified @@ -758,19 +906,24 @@

Parameters

else: fig = None - _plot_(ax, - extracted_EEGData, - sampling_frequency, - start_sec, - stop_sec, - x_scale, - y_scale, - make_legend ) - + pvalues = _plot_(ax, + extracted_EEGData, + sampling_frequency, + start_sec, + stop_sec, + x_scale, + y_scale, + baseline, + make_legend, + significance_level = significance_level + ) + # we show the figure only if no ax was provided # and thus no "bigger" figure is assembled elsewhere... if fig is not None: - plt.show()
+ plt.show() + + return pvalues
diff --git a/docs/EEGToolkit/EEGStats/EEGStats.html b/docs/EEGToolkit/EEGStats/EEGStats.html new file mode 100644 index 0000000..e19ea90 --- /dev/null +++ b/docs/EEGToolkit/EEGStats/EEGStats.html @@ -0,0 +1,1007 @@ + + + + + + +EEGToolkit.EEGStats.EEGStats API documentation + + + + + + + + + + + +
+
+
+

Module EEGToolkit.EEGStats.EEGStats

+
+
+

Computes time-point-wise the mean and SEM values of EEG data stored in numpy ndarrays.

+
+ +Expand source code + +
"""
+Computes time-point-wise the mean and SEM values of EEG data stored in numpy ndarrays.
+"""
+
+import numpy as np
+from statsmodels.stats.weightstats import CompareMeans, DescrStatsW
+import matplotlib.pyplot as plt
+
+# for custom legend
+from matplotlib.lines import Line2D
+from matplotlib.patches import Patch
+
+# ----------------------------------------------------------------
+#                        Data Statistics 
+# ----------------------------------------------------------------
+
+
+def mean(extracted_EEGData:np.ndarray) -> np.ndarray:
+    """
+    Computes the element-wise mean of an `m x n numpy ndarray` 
+    with `m` repeated datasets and `n` entries per set along axis 1
+    (between repeated sets).
+
+    Parameters
+    ----------
+    extracted_EEGData : np.ndarray
+        An m x n numpy ndarray containing EEG data
+
+    Returns
+    -------
+    means : np.ndarray
+        An 1 x n numpy ndarray containing the element-wise means between all m repeated sets.
+    """
+    means = DescrStatsW( extracted_EEGData ).mean
+    return means
+
+def sem(extracted_EEGData:np.ndarray) -> np.ndarray:
+    """
+    Compute the standard error of the mean (SEM)
+    of an `m x n numpy ndarray` with `m` repeated 
+    datasets and `n` entries per set along axis 1
+    (between repeated sets).
+
+    Parameters
+    ----------
+    extracted_EEGData : np.ndarray
+        An m x n numpy ndarray containing EEG data
+
+    Returns
+    -------
+    sems : np.ndarray
+        An 1 x n numpy ndarray containing the SEM between all m repeated sets.
+
+    """
+    sems = DescrStatsW( extracted_EEGData ) 
+    length = len( extracted_EEGData )
+    sems = sems.std / np.sqrt( length )
+    return sems
+
+
+
+def compare_signals(
+                        extracted_EEGData_1 : np.ndarray, 
+                        extracted_EEGData_2 : np.ndarray
+                    ) -> np.ndarray:
+    """
+    Compares two EEG signal datasets stored as identical `m x n` np ndarrays
+    with `m` repeated datasets and `n` entries per set, element-wise (along axis 1).
+    It performs T-Tests to check for siginificant differences between the two datasets
+    and returns the corresoponding p-values as a new array.
+
+    Parameters
+    ----------
+    extracted_EEGData_1 : np.ndarray
+        The first EEG signal dataset.
+        
+    extracted_EEGData_2 : np.ndarray
+        The second EEG signal dataset.
+    
+    Returns
+    -----
+    pvalues : np.ndarray
+        A 1 x n numpy ndarray of p-values for the 
+        difference between Signals 1 and 2 at each position.
+    """
+
+    # compare the signal means
+    diff = CompareMeans.from_data(
+                                    extracted_EEGData_1, 
+                                    extracted_EEGData_2
+                                )
+
+    # perform t-test comparison
+    diff = diff.ttest_ind()
+
+    # get the p-values 
+    pvalues = diff[1]
+    return pvalues
+
+# ----------------------------------------------------------------
+#                        Data Visualisation 
+# ----------------------------------------------------------------
+
+# set up some default style settings 
+
+# colorscheme
+signal_color = "gray"
+signal1_color = "xkcd:dark royal blue"
+signal2_color = "xkcd:watermelon"
+signif_shade_color = "xkcd:yellow tan"
+
+# opacities
+signal_alpha = 0.8
+sem_alpha = 0.2
+signif_shade_alpha = 0.5
+
+def plot_signal(
+                    extracted_EEGData : np.ndarray,
+                    sampling_frequency : float,
+                    start_sec : float,
+                    stop_sec : float,
+                    x_scale = 10**3,
+                    y_scale = 10**3,
+                    baseline : np.ndarray = None,
+                    significance_level:float = 0.05,
+                    make_legend = False,
+                    ax = None ) -> None:
+    """
+    Visualises a single EGG signal dataset from an `m x n numpy ndarray`
+    with `m` repeated datasets and `n` entries per set. It generates a solid
+    line for the time-point-wise mean and a shaded area of the corresponding SEM. 
+
+    Parameters
+    ----------
+
+    extracted_EEGData : np.ndarray
+        An m x n numpy ndarray of repeated EEG data.
+    
+    sampling_frequency : float
+        The frequency in which the EEG data was recorded in `Hertz`.
+    
+    start_sec : float  
+        The initial / low bound of the time-scale in seconds.
+    
+    stop_sec : float
+        The final / upper bound of the time-scale in seconds.
+
+    x_scale : float
+        A scaling factor to adjust the data's x-value range. 
+        E.g. `x_scale = 1000` to adjust the time-scale to milliseconds.
+    
+    y_scale : float
+        A scaling factor for the data's y-value range.
+        E.g. `y_scale = 1000` to adjust the signal-scale to millivolts.
+    
+    baseline : np.ndarray
+        An array of baseline data to compare the signal to using a pair-wise t-test.
+        This will introduce shaded boxes for regions that show significant differences
+        between the signal and the baseline.
+    
+    significance_level : float
+        The significance threshold for which to accept a signal difference
+        as significant. Default is `0.05`.
+
+    make_legend : bool
+        Generates a legend for the plot.
+
+    Returns
+    -----
+    pvalues : np.ndarray    
+        The p-value from pair-wise T-Test comparison between the 
+        signal and the baseline (if provided) at each signal position (timepoint).
+    """
+
+    # generate a new figure if no ax is specified
+    if ax is None:
+        fig, ax = plt.subplots()
+    else: 
+        fig = None
+
+    pvalues = _plot_(ax,
+                    extracted_EEGData,
+                    sampling_frequency,
+                    start_sec,
+                    stop_sec,
+                    x_scale,
+                    y_scale, 
+                    baseline,
+                    make_legend,
+                    significance_level = significance_level 
+                )
+
+    # we show the figure only if no ax was provided
+    # and thus no "bigger" figure is assembled elsewhere...
+    if fig is not None:
+        plt.show()
+    
+    return pvalues
+
+def difference_plot(extracted_EEGData_1:np.ndarray,
+                    extracted_EEGData_2:np.ndarray,
+                    sampling_frequency:float,
+                    start_sec:float,
+                    stop_sec:float,
+                    significance_level:float = 0.05,
+                    x_scale=10**3,
+                    y_scale=10**3,
+                    make_legend = False, 
+                    ax = None ) -> None:
+
+    """
+    Visualises the difference between two EEG signals and tests
+    time-point-wise the difference using T-Tests. Individual EEG
+    Signals are shown as mean-line with shaded SEM, and time-points
+    with significant differences are highlighted with overall-shading.
+
+    Parameters
+    ----------
+    extracted_EEGData_1 : np.ndarray
+        The first EEG signal dataset.
+        
+    extracted_EEGData_2 : np.ndarray
+        The second EEG signal dataset.
+    
+    sampling_frequency : float
+        The frequency in which the EEG data was recorded in `Hertz`.
+    
+    significance_level : float
+        The significance threshold for which to accept a signal difference
+        as significant. Default is `0.05`.
+
+    start_sec : float  
+        The initial / low bound of the time-scale in seconds.
+
+    stop_sec : float
+        The final / upper bound of the time-scale in seconds.
+
+    x_scale : float
+        A scaling factor to adjust the data's x-value range. 
+        E.g. `x_scale = 1000` to adjust the time-scale to milliseconds.
+    
+    y_scale : float
+        A scaling factor for the data's y-value range.
+        E.g. `y_scale = 1000` to adjust the signal-scale to millivolts.
+    
+    make_legend : bool
+        Generates a legend for the plot.
+    
+    Returns
+    -----
+    pvalues : np.ndarray    
+        The p-value from pair-wise T-Test comparison between the 
+        two signals at each signal position (timepoint).
+    """
+
+    # generate a new figure if no ax is given
+    if ax is None:
+        fig, ax = plt.subplots()
+    else: 
+        fig = None
+
+    pvalues = _difference_plot_(ax,
+                        extracted_EEGData_1,
+                        extracted_EEGData_2,
+                        significance_level,
+                        sampling_frequency,
+                        start_sec,
+                        stop_sec,
+                        x_scale,
+                        y_scale,
+                        make_legend 
+                        )
+
+    # we show the figure only if no ax was provided
+    # and thus no "bigger" figure is assembled elsewhere...
+    if fig is not None: 
+        plt.show()
+
+    return pvalues
+
+def _plot_(
+            ax, 
+            extracted_EEGData:np.ndarray,
+            sampling_frequency:float,
+            start_sec:float,
+            stop_sec:float,
+            x_scale=10**3,
+            y_scale=10**3, 
+            baseline : np.ndarray = None, 
+            make_legend = False,
+            **kwargs ) -> None:
+    """
+    Generates a mean signal line with shaded 
+    SEM area from an m x n numpy ndarray.
+
+    Note
+    -----
+    This is the core for `plot_signal` 
+    """
+    # compute mean and SEM for the signal
+    _mean = mean(extracted_EEGData)
+    _sem = sem(extracted_EEGData)
+
+    # now generate scaled xvalues
+    x_values = _scale_xboundries(sampling_frequency, start_sec, stop_sec, x_scale)
+
+    # now plot the signal's scaled mean line
+    signal = _mean * y_scale 
+    ax.plot(x_values, signal, color = signal_color )
+
+    # and add the shading for SEM 
+    lower = (_mean - _sem) 
+    upper = (_mean + _sem)
+    lower *= y_scale
+    upper *= y_scale
+
+    ax.fill_between(
+                        x_values,
+                        lower,
+                        upper,
+                        color = signal_color,
+                        edgecolor = None,
+                        linewidth = 0,
+                        alpha = sem_alpha
+                        
+                )
+
+    # if we have baseline data, we compare also to the baseline
+    # and shade siginificantly different regions...
+    pvalues = None
+    if baseline is not None:
+
+        pvalues = compare_signals( extracted_EEGData, baseline )
+
+        # generate the y-value boundries for the plot
+        # and scale the y-values to some user-defined range
+        # plus add a little padding
+        max_y = np.max( _mean )
+        min_y = np.min( _mean )
+        yvalues = _scale_yboundries( y_scale, max_y, min_y, pad = 1.2 )
+
+        # add the shaded fillings for significantly different
+        # timepoint areas
+        signif_level = kwargs.pop( "significance_level", 0.05 )
+        _shade_singificant_regions(
+                                    ax, 
+                                    significance_level = signif_level, 
+                                    pvalues = pvalues, 
+                                    xvalues = x_values,
+                                    ylim = yvalues, 
+                                )
+
+
+
+    # and add a line for the start of the signal
+    ax.axvline( x=0, linewidth = 2, color = "black" )
+
+    # and some axes formatting...
+    ax.set_title("Average EEG Signal (Shaded area SEM)")
+    ax.set_ylabel("Signal\namplitude")
+    ax.set_xlabel("Time relative to event")
+    
+    if make_legend:
+        handles = [
+                    Line2D(     [0], [0], 
+                                color = signal_color, 
+                                label = "Mean Signal" 
+                        ),
+                    Patch( 
+                                facecolor = signal_color, 
+                                edgecolor = None, linewidth = 0,
+                                alpha = sem_alpha,
+                                label = "SEM"
+                        )
+                ]
+        if baseline is not None: 
+            handles.append(
+                            Patch( 
+                                    facecolor = signif_shade_color, 
+                                    edgecolor = None, linewidth = 0,
+                                    alpha = signif_shade_alpha,
+                                    label = f"pvalue < {signif_level}"
+                            )
+                        )
+        # now add a custom legend
+        ax.legend( handles = handles,
+                            bbox_to_anchor = (1, -0.5),
+                            frameon = False
+                        )
+    return pvalues
+
+def _difference_plot_(ax,
+                      extracted_EEGData_1:np.ndarray,
+                      extracted_EEGData_2:np.ndarray,
+                      significance_level:float,
+                      sampling_frequency:float,
+                      start_sec:float,
+                      stop_sec:float,
+                      x_scale=10**3,
+                      y_scale=10**3,
+                      make_legend = False ) -> None: 
+
+    """
+    Generates a plot of two EEG signals and their time-point-wise 
+    difference using a T-Test. 
+
+    Note
+    -----
+    This is the core of difference_plot
+    """
+
+    # compare the two EEG signals and generate a p-value 
+    # from t-test position-wise comparisons...
+    pvalues = compare_signals(extracted_EEGData_1, extracted_EEGData_2)
+
+    # generate the mean lines for both signals
+    mean_1 = mean(extracted_EEGData_1)
+    mean_2 = mean(extracted_EEGData_2)
+
+    # generate the y-value boundries for the plot
+    max_y = np.max( [np.max(mean_1), np.max(mean_2)] )
+    min_y = np.min( [np.min(mean_1), np.min(mean_2)] )
+
+    # and scale the y-values to some user-defined range
+    # plus add a little padding
+    yvalues = _scale_yboundries( y_scale, max_y, min_y, pad = 1.2 )
+
+    # generate correspondingly scaled x-values
+    x_values = _scale_xboundries(sampling_frequency, start_sec, stop_sec, x_scale)
+
+    # add the shaded fillings for significantly different
+    # timepoint areas
+    _shade_singificant_regions(
+                                ax, 
+                                significance_level = significance_level, 
+                                pvalues = pvalues, 
+                                xvalues = x_values,
+                                ylim = yvalues, 
+                            )
+
+    # plot scaled signals 1 and 2
+    signal_1 = mean_1 * y_scale
+    signal_2 = mean_2 * y_scale
+
+    ax.plot(x_values, signal_1, color = signal1_color, alpha = signal_alpha )
+    ax.plot(x_values, signal_2, color = signal2_color, alpha = signal_alpha )
+
+    # plot a vertical line at the signal start
+    ax.axvline(x = 0, color = "black", linewidth = 2 )
+
+    # and some axes formatting...
+    ax.set_title("Average EEG Signal (Shaded area significant regions)")
+    ax.set_ylabel("Signal\namplitude")
+    ax.set_xlabel("Time relative to event")
+
+    if make_legend: 
+        # now add a custom legend
+        ax.legend( handles = [
+                                Line2D(     [0], [0], 
+                                            color = signal1_color, 
+                                            label = "Mean horizontal Signal" 
+                                    ),
+                                Line2D(     [0], [0], 
+                                            color = signal2_color, 
+                                            label = "Mean vertical Signal" 
+                                    ),
+                                Patch( 
+                                            facecolor = signif_shade_color, 
+                                            edgecolor = None, linewidth = 0,
+                                            alpha = signif_shade_alpha,
+                                            label = f"pvalue < {significance_level}"
+                                    )
+
+                            ],
+                            bbox_to_anchor = (1, -0.5),
+                            frameon = False
+                        )
+    return pvalues
+
+def _shade_singificant_regions(ax, significance_level : float , pvalues : np.ndarray , xvalues : np.ndarray , ylim : tuple ):
+    """
+    Shades the background of regions (x-value ranges) where corresponding p-values
+    are below a given significance level. 
+
+    Parameters
+    ----------
+    ax : plt.axes.Axes 
+        An Axes object to plot to.
+    significance_level : float
+        The significance level to use.
+    pvalues : np.ndarray
+        An array of pvalues for each x-value.
+    xvalues : np.ndarray
+        An array of x-values.
+    ylim : tuple
+        A tuple of minimal and maximal y-values to shade.
+    """
+    ax.fill_between(
+                     xvalues,
+                     ylim[0],
+                     ylim[1],
+                     # now fill areas where the pvalues are 
+                     # below our significance_level
+                     where = pvalues < significance_level, 
+                     facecolor = signif_shade_color,
+                     alpha = signif_shade_alpha
+                )
+
+def _scale_xboundries(sampling_frequency, start_sec, stop_sec, x_scale):
+    """
+    Scales minimal and maximal x values from starting and final timestep values
+    given some scale and sampling frequency.
+    """
+    x_values = np.arange(
+                            int( start_sec * sampling_frequency ),
+                            int( stop_sec * sampling_frequency ), 
+                            dtype=int
+                        )
+    x_values = x_values / sampling_frequency * x_scale
+    return x_values
+
+def _scale_yboundries(y_scale, max_y, min_y, pad = 1 ):
+    """
+    Scales minimal and maximal y values from some data
+    by a given scale and adds additional padding as a scalar factor
+    (pad = 1 means no padding).
+    """
+    max_y *= y_scale * pad
+    min_y *= y_scale * pad
+    return min_y, max_y
+
+
+
+
+
+
+
+

Functions

+
+
+def compare_signals(extracted_EEGData_1: numpy.ndarray, extracted_EEGData_2: numpy.ndarray) ‑> numpy.ndarray +
+
+

Compares two EEG signal datasets stored as identical m x n np ndarrays +with m repeated datasets and n entries per set, element-wise (along axis 1). +It performs T-Tests to check for siginificant differences between the two datasets +and returns the corresoponding p-values as a new array.

+

Parameters

+
+
extracted_EEGData_1 : np.ndarray
+
The first EEG signal dataset.
+
extracted_EEGData_2 : np.ndarray
+
The second EEG signal dataset.
+
+

Returns

+
+
pvalues : np.ndarray
+
A 1 x n numpy ndarray of p-values for the +difference between Signals 1 and 2 at each position.
+
+
+ +Expand source code + +
def compare_signals(
+                        extracted_EEGData_1 : np.ndarray, 
+                        extracted_EEGData_2 : np.ndarray
+                    ) -> np.ndarray:
+    """
+    Compares two EEG signal datasets stored as identical `m x n` np ndarrays
+    with `m` repeated datasets and `n` entries per set, element-wise (along axis 1).
+    It performs T-Tests to check for siginificant differences between the two datasets
+    and returns the corresoponding p-values as a new array.
+
+    Parameters
+    ----------
+    extracted_EEGData_1 : np.ndarray
+        The first EEG signal dataset.
+        
+    extracted_EEGData_2 : np.ndarray
+        The second EEG signal dataset.
+    
+    Returns
+    -----
+    pvalues : np.ndarray
+        A 1 x n numpy ndarray of p-values for the 
+        difference between Signals 1 and 2 at each position.
+    """
+
+    # compare the signal means
+    diff = CompareMeans.from_data(
+                                    extracted_EEGData_1, 
+                                    extracted_EEGData_2
+                                )
+
+    # perform t-test comparison
+    diff = diff.ttest_ind()
+
+    # get the p-values 
+    pvalues = diff[1]
+    return pvalues
+
+
+
+def difference_plot(extracted_EEGData_1: numpy.ndarray, extracted_EEGData_2: numpy.ndarray, sampling_frequency: float, start_sec: float, stop_sec: float, significance_level: float = 0.05, x_scale=1000, y_scale=1000, make_legend=False, ax=None) ‑> None +
+
+

Visualises the difference between two EEG signals and tests +time-point-wise the difference using T-Tests. Individual EEG +Signals are shown as mean-line with shaded SEM, and time-points +with significant differences are highlighted with overall-shading.

+

Parameters

+
+
extracted_EEGData_1 : np.ndarray
+
The first EEG signal dataset.
+
extracted_EEGData_2 : np.ndarray
+
The second EEG signal dataset.
+
sampling_frequency : float
+
The frequency in which the EEG data was recorded in Hertz.
+
significance_level : float
+
The significance threshold for which to accept a signal difference +as significant. Default is 0.05.
+
start_sec : float +
+
The initial / low bound of the time-scale in seconds.
+
stop_sec : float
+
The final / upper bound of the time-scale in seconds.
+
x_scale : float
+
A scaling factor to adjust the data's x-value range. +E.g. x_scale = 1000 to adjust the time-scale to milliseconds.
+
y_scale : float
+
A scaling factor for the data's y-value range. +E.g. y_scale = 1000 to adjust the signal-scale to millivolts.
+
make_legend : bool
+
Generates a legend for the plot.
+
+

Returns

+
+
pvalues : np.ndarray +
+
The p-value from pair-wise T-Test comparison between the +two signals at each signal position (timepoint).
+
+
+ +Expand source code + +
def difference_plot(extracted_EEGData_1:np.ndarray,
+                    extracted_EEGData_2:np.ndarray,
+                    sampling_frequency:float,
+                    start_sec:float,
+                    stop_sec:float,
+                    significance_level:float = 0.05,
+                    x_scale=10**3,
+                    y_scale=10**3,
+                    make_legend = False, 
+                    ax = None ) -> None:
+
+    """
+    Visualises the difference between two EEG signals and tests
+    time-point-wise the difference using T-Tests. Individual EEG
+    Signals are shown as mean-line with shaded SEM, and time-points
+    with significant differences are highlighted with overall-shading.
+
+    Parameters
+    ----------
+    extracted_EEGData_1 : np.ndarray
+        The first EEG signal dataset.
+        
+    extracted_EEGData_2 : np.ndarray
+        The second EEG signal dataset.
+    
+    sampling_frequency : float
+        The frequency in which the EEG data was recorded in `Hertz`.
+    
+    significance_level : float
+        The significance threshold for which to accept a signal difference
+        as significant. Default is `0.05`.
+
+    start_sec : float  
+        The initial / low bound of the time-scale in seconds.
+
+    stop_sec : float
+        The final / upper bound of the time-scale in seconds.
+
+    x_scale : float
+        A scaling factor to adjust the data's x-value range. 
+        E.g. `x_scale = 1000` to adjust the time-scale to milliseconds.
+    
+    y_scale : float
+        A scaling factor for the data's y-value range.
+        E.g. `y_scale = 1000` to adjust the signal-scale to millivolts.
+    
+    make_legend : bool
+        Generates a legend for the plot.
+    
+    Returns
+    -----
+    pvalues : np.ndarray    
+        The p-value from pair-wise T-Test comparison between the 
+        two signals at each signal position (timepoint).
+    """
+
+    # generate a new figure if no ax is given
+    if ax is None:
+        fig, ax = plt.subplots()
+    else: 
+        fig = None
+
+    pvalues = _difference_plot_(ax,
+                        extracted_EEGData_1,
+                        extracted_EEGData_2,
+                        significance_level,
+                        sampling_frequency,
+                        start_sec,
+                        stop_sec,
+                        x_scale,
+                        y_scale,
+                        make_legend 
+                        )
+
+    # we show the figure only if no ax was provided
+    # and thus no "bigger" figure is assembled elsewhere...
+    if fig is not None: 
+        plt.show()
+
+    return pvalues
+
+
+
+def mean(extracted_EEGData: numpy.ndarray) ‑> numpy.ndarray +
+
+

Computes the element-wise mean of an m x n numpy ndarray +with m repeated datasets and n entries per set along axis 1 +(between repeated sets).

+

Parameters

+
+
extracted_EEGData : np.ndarray
+
An m x n numpy ndarray containing EEG data
+
+

Returns

+
+
means : np.ndarray
+
An 1 x n numpy ndarray containing the element-wise means between all m repeated sets.
+
+
+ +Expand source code + +
def mean(extracted_EEGData:np.ndarray) -> np.ndarray:
+    """
+    Computes the element-wise mean of an `m x n numpy ndarray` 
+    with `m` repeated datasets and `n` entries per set along axis 1
+    (between repeated sets).
+
+    Parameters
+    ----------
+    extracted_EEGData : np.ndarray
+        An m x n numpy ndarray containing EEG data
+
+    Returns
+    -------
+    means : np.ndarray
+        An 1 x n numpy ndarray containing the element-wise means between all m repeated sets.
+    """
+    means = DescrStatsW( extracted_EEGData ).mean
+    return means
+
+
+
+def plot_signal(extracted_EEGData: numpy.ndarray, sampling_frequency: float, start_sec: float, stop_sec: float, x_scale=1000, y_scale=1000, baseline: numpy.ndarray = None, significance_level: float = 0.05, make_legend=False, ax=None) ‑> None +
+
+

Visualises a single EGG signal dataset from an m x n numpy ndarray +with m repeated datasets and n entries per set. It generates a solid +line for the time-point-wise mean and a shaded area of the corresponding SEM.

+

Parameters

+
+
extracted_EEGData : np.ndarray
+
An m x n numpy ndarray of repeated EEG data.
+
sampling_frequency : float
+
The frequency in which the EEG data was recorded in Hertz.
+
start_sec : float +
+
The initial / low bound of the time-scale in seconds.
+
stop_sec : float
+
The final / upper bound of the time-scale in seconds.
+
x_scale : float
+
A scaling factor to adjust the data's x-value range. +E.g. x_scale = 1000 to adjust the time-scale to milliseconds.
+
y_scale : float
+
A scaling factor for the data's y-value range. +E.g. y_scale = 1000 to adjust the signal-scale to millivolts.
+
baseline : np.ndarray
+
An array of baseline data to compare the signal to using a pair-wise t-test. +This will introduce shaded boxes for regions that show significant differences +between the signal and the baseline.
+
significance_level : float
+
The significance threshold for which to accept a signal difference +as significant. Default is 0.05.
+
make_legend : bool
+
Generates a legend for the plot.
+
+

Returns

+
+
pvalues : np.ndarray +
+
The p-value from pair-wise T-Test comparison between the +signal and the baseline (if provided) at each signal position (timepoint).
+
+
+ +Expand source code + +
def plot_signal(
+                    extracted_EEGData : np.ndarray,
+                    sampling_frequency : float,
+                    start_sec : float,
+                    stop_sec : float,
+                    x_scale = 10**3,
+                    y_scale = 10**3,
+                    baseline : np.ndarray = None,
+                    significance_level:float = 0.05,
+                    make_legend = False,
+                    ax = None ) -> None:
+    """
+    Visualises a single EGG signal dataset from an `m x n numpy ndarray`
+    with `m` repeated datasets and `n` entries per set. It generates a solid
+    line for the time-point-wise mean and a shaded area of the corresponding SEM. 
+
+    Parameters
+    ----------
+
+    extracted_EEGData : np.ndarray
+        An m x n numpy ndarray of repeated EEG data.
+    
+    sampling_frequency : float
+        The frequency in which the EEG data was recorded in `Hertz`.
+    
+    start_sec : float  
+        The initial / low bound of the time-scale in seconds.
+    
+    stop_sec : float
+        The final / upper bound of the time-scale in seconds.
+
+    x_scale : float
+        A scaling factor to adjust the data's x-value range. 
+        E.g. `x_scale = 1000` to adjust the time-scale to milliseconds.
+    
+    y_scale : float
+        A scaling factor for the data's y-value range.
+        E.g. `y_scale = 1000` to adjust the signal-scale to millivolts.
+    
+    baseline : np.ndarray
+        An array of baseline data to compare the signal to using a pair-wise t-test.
+        This will introduce shaded boxes for regions that show significant differences
+        between the signal and the baseline.
+    
+    significance_level : float
+        The significance threshold for which to accept a signal difference
+        as significant. Default is `0.05`.
+
+    make_legend : bool
+        Generates a legend for the plot.
+
+    Returns
+    -----
+    pvalues : np.ndarray    
+        The p-value from pair-wise T-Test comparison between the 
+        signal and the baseline (if provided) at each signal position (timepoint).
+    """
+
+    # generate a new figure if no ax is specified
+    if ax is None:
+        fig, ax = plt.subplots()
+    else: 
+        fig = None
+
+    pvalues = _plot_(ax,
+                    extracted_EEGData,
+                    sampling_frequency,
+                    start_sec,
+                    stop_sec,
+                    x_scale,
+                    y_scale, 
+                    baseline,
+                    make_legend,
+                    significance_level = significance_level 
+                )
+
+    # we show the figure only if no ax was provided
+    # and thus no "bigger" figure is assembled elsewhere...
+    if fig is not None:
+        plt.show()
+    
+    return pvalues
+
+
+
+def sem(extracted_EEGData: numpy.ndarray) ‑> numpy.ndarray +
+
+

Compute the standard error of the mean (SEM) +of an m x n numpy ndarray with m repeated +datasets and n entries per set along axis 1 +(between repeated sets).

+

Parameters

+
+
extracted_EEGData : np.ndarray
+
An m x n numpy ndarray containing EEG data
+
+

Returns

+
+
sems : np.ndarray
+
An 1 x n numpy ndarray containing the SEM between all m repeated sets.
+
+
+ +Expand source code + +
def sem(extracted_EEGData:np.ndarray) -> np.ndarray:
+    """
+    Compute the standard error of the mean (SEM)
+    of an `m x n numpy ndarray` with `m` repeated 
+    datasets and `n` entries per set along axis 1
+    (between repeated sets).
+
+    Parameters
+    ----------
+    extracted_EEGData : np.ndarray
+        An m x n numpy ndarray containing EEG data
+
+    Returns
+    -------
+    sems : np.ndarray
+        An 1 x n numpy ndarray containing the SEM between all m repeated sets.
+
+    """
+    sems = DescrStatsW( extracted_EEGData ) 
+    length = len( extracted_EEGData )
+    sems = sems.std / np.sqrt( length )
+    return sems
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/EEGToolkit/EEGStats/index.html b/docs/EEGToolkit/EEGStats/index.html new file mode 100644 index 0000000..1fb80e6 --- /dev/null +++ b/docs/EEGToolkit/EEGStats/index.html @@ -0,0 +1,71 @@ + + + + + + +EEGToolkit.EEGStats API documentation + + + + + + + + + + + +
+
+
+

Module EEGToolkit.EEGStats

+
+
+
+ +Expand source code + +
from .EEGStats import *
+
+
+
+

Sub-modules

+
+
EEGToolkit.EEGStats.EEGStats
+
+

Computes time-point-wise the mean and SEM values of EEG data stored in numpy ndarrays.

+
+
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/EEGToolkit/auxiliary.html b/docs/EEGToolkit/auxiliary.html new file mode 100644 index 0000000..1f7dd30 --- /dev/null +++ b/docs/EEGToolkit/auxiliary.html @@ -0,0 +1,613 @@ + + + + + + +EEGToolkit.auxiliary API documentation + + + + + + + + + + + +
+
+
+

Module EEGToolkit.auxiliary

+
+
+

Auxiliary functions to work within the streamlit environment

+
+ +Expand source code + +
"""
+Auxiliary functions to work within the streamlit environment
+"""
+
+from .EEGData import EEGData, supported_filetypes
+import streamlit as st 
+from copy import deepcopy
+import os 
+
+class Session:
+    """
+    A class to handle storing values into and getting them from the streamlit session state.
+    """
+    def __init__(self):
+
+        # a list of keys to include in the 
+        # export session dictionary
+        self._to_export = set()
+
+    def add( self, key, value, export = True ):
+        """
+        Add a new item to the session but 
+        can also change an existing item.
+
+        Parameters
+        ----------
+        key : str
+            The key of the new item
+        value 
+            The value to store of the new item
+        export : bool
+            Wether or not to include the item in the exported session dictioanry.
+        """
+        st.session_state[ key ] = value
+
+        if export: 
+            self._to_export.add( key )
+    
+    def set( self, key, value, export = True ):
+        """
+        A synonym of `add`
+        """
+        self.add( key, value, export )
+
+    def remove( self, key ):
+        """
+        Remove an item from the session
+        """
+        try: 
+            st.session_state.pop( key )
+            self._to_export.remove( key )
+        except: 
+            pass
+
+    def get( self, key, default = None, rm = False, copy = False ):
+        """
+        Tries to get an item from the session 
+        and returns the default if this fails. 
+        If rm = True is set, the item will be 
+        removed after getting. 
+        If copy = True is set, it will return a deepcopy.
+        """
+        try: 
+            value = st.session_state[ key ]
+            if copy: 
+                value = deepcopy(value)
+        except: 
+            value = default
+
+        if rm: 
+            self.remove( key )
+
+        return value 
+
+    def setup( self, key, value ):
+        """
+        Stores an item to the session only if it's not already present.
+        """
+        if not self.exists( key ):
+            self.add( key, value )
+
+    def exists( self, key ):
+        """
+        Checks if an item is in the session. Returns True if so.
+        """
+        verdict = key in st.session_state
+        return verdict
+    
+    def hasvalue( self, key ):
+        """
+        Checks if an item is in the session and is also not None.
+        Returns True if both conditions are fulfilled.
+        """
+        exists = self.exists( key )
+        not_none = self.get( key, None ) is not None
+        verdict = exists and not_none
+        return verdict
+
+    def export( self ):
+        """
+        Exports a copy of the session state for better readablity wherein non-intelligible entries are removed. 
+        """
+        to_export = st.session_state[ self._to_export ]
+        return to_export
+
+
+
+class stEEGData( EEGData ):
+    """
+    A streamlit compatible version of EEGData
+    The primary difference here is that it replaces the original filepath variables
+    with filepath.name arguments to work with the string and not the UploadedFile objects.
+    """
+    def __init__( self, *args, **kwargs ):
+        super().__init__(*args, **kwargs)
+        
+    def _filesuffix(self, filepath):
+        """
+        Returns the suffix from a filepath
+        """
+        suffix = os.path.basename( filepath.name )
+        suffix = suffix.split(".")[-1]
+        return suffix
+
+    def _csv_delimiter(self, filepath):
+        """
+        Checks if a csv file is , or ; delimited and returns the 
+        correct delmiter to use...
+        """
+        # open the file and read 
+        content = deepcopy(filepath).read().decode()
+
+        # check if a semicolon is present
+        # if so, we delimit at ; 
+        has_semicolon = ";" in content
+        delimiter = ";" if has_semicolon else ","
+
+        return delimiter
+    
+    def _check_sanity(self, signal_path, event_path, sampling_frequency):
+        """
+        Checks if valid data inputs were provided
+        """
+
+        # check if the datafiles conform to suppored filetypes
+        fname = os.path.basename(signal_path.name)
+        if not any( [ fname.endswith(suffix) for suffix in supported_filetypes ] ):
+            suffix = fname.split(".")[-1]
+            raise TypeError( f"The signal datafile could not be interpreted ('.{suffix}'), only {supported_filetypes} files are supported!" )
+
+        fname = os.path.basename(event_path.name)
+        if not any( [ fname.endswith(suffix) for suffix in supported_filetypes ] ):
+            suffix = fname.split(".")[-1]
+            raise TypeError( f"The event datafile could not be interpreted ('.{suffix}'), only {supported_filetypes} files are supported!" )
+        
+        # check if the frequency is a positive number
+        if not sampling_frequency > 0:
+            raise ValueError( f"Sampling frequency {sampling_frequency} must be a positive value" )
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Session +
+
+

A class to handle storing values into and getting them from the streamlit session state.

+
+ +Expand source code + +
class Session:
+    """
+    A class to handle storing values into and getting them from the streamlit session state.
+    """
+    def __init__(self):
+
+        # a list of keys to include in the 
+        # export session dictionary
+        self._to_export = set()
+
+    def add( self, key, value, export = True ):
+        """
+        Add a new item to the session but 
+        can also change an existing item.
+
+        Parameters
+        ----------
+        key : str
+            The key of the new item
+        value 
+            The value to store of the new item
+        export : bool
+            Wether or not to include the item in the exported session dictioanry.
+        """
+        st.session_state[ key ] = value
+
+        if export: 
+            self._to_export.add( key )
+    
+    def set( self, key, value, export = True ):
+        """
+        A synonym of `add`
+        """
+        self.add( key, value, export )
+
+    def remove( self, key ):
+        """
+        Remove an item from the session
+        """
+        try: 
+            st.session_state.pop( key )
+            self._to_export.remove( key )
+        except: 
+            pass
+
+    def get( self, key, default = None, rm = False, copy = False ):
+        """
+        Tries to get an item from the session 
+        and returns the default if this fails. 
+        If rm = True is set, the item will be 
+        removed after getting. 
+        If copy = True is set, it will return a deepcopy.
+        """
+        try: 
+            value = st.session_state[ key ]
+            if copy: 
+                value = deepcopy(value)
+        except: 
+            value = default
+
+        if rm: 
+            self.remove( key )
+
+        return value 
+
+    def setup( self, key, value ):
+        """
+        Stores an item to the session only if it's not already present.
+        """
+        if not self.exists( key ):
+            self.add( key, value )
+
+    def exists( self, key ):
+        """
+        Checks if an item is in the session. Returns True if so.
+        """
+        verdict = key in st.session_state
+        return verdict
+    
+    def hasvalue( self, key ):
+        """
+        Checks if an item is in the session and is also not None.
+        Returns True if both conditions are fulfilled.
+        """
+        exists = self.exists( key )
+        not_none = self.get( key, None ) is not None
+        verdict = exists and not_none
+        return verdict
+
+    def export( self ):
+        """
+        Exports a copy of the session state for better readablity wherein non-intelligible entries are removed. 
+        """
+        to_export = st.session_state[ self._to_export ]
+        return to_export
+
+

Methods

+
+
+def add(self, key, value, export=True) +
+
+

Add a new item to the session but +can also change an existing item.

+

Parameters

+
+
key : str
+
The key of the new item
+
value
+
The value to store of the new item
+
export : bool
+
Wether or not to include the item in the exported session dictioanry.
+
+
+ +Expand source code + +
def add( self, key, value, export = True ):
+    """
+    Add a new item to the session but 
+    can also change an existing item.
+
+    Parameters
+    ----------
+    key : str
+        The key of the new item
+    value 
+        The value to store of the new item
+    export : bool
+        Wether or not to include the item in the exported session dictioanry.
+    """
+    st.session_state[ key ] = value
+
+    if export: 
+        self._to_export.add( key )
+
+
+
+def exists(self, key) +
+
+

Checks if an item is in the session. Returns True if so.

+
+ +Expand source code + +
def exists( self, key ):
+    """
+    Checks if an item is in the session. Returns True if so.
+    """
+    verdict = key in st.session_state
+    return verdict
+
+
+
+def export(self) +
+
+

Exports a copy of the session state for better readablity wherein non-intelligible entries are removed.

+
+ +Expand source code + +
def export( self ):
+    """
+    Exports a copy of the session state for better readablity wherein non-intelligible entries are removed. 
+    """
+    to_export = st.session_state[ self._to_export ]
+    return to_export
+
+
+
+def get(self, key, default=None, rm=False, copy=False) +
+
+

Tries to get an item from the session +and returns the default if this fails. +If rm = True is set, the item will be +removed after getting. +If copy = True is set, it will return a deepcopy.

+
+ +Expand source code + +
def get( self, key, default = None, rm = False, copy = False ):
+    """
+    Tries to get an item from the session 
+    and returns the default if this fails. 
+    If rm = True is set, the item will be 
+    removed after getting. 
+    If copy = True is set, it will return a deepcopy.
+    """
+    try: 
+        value = st.session_state[ key ]
+        if copy: 
+            value = deepcopy(value)
+    except: 
+        value = default
+
+    if rm: 
+        self.remove( key )
+
+    return value 
+
+
+
+def hasvalue(self, key) +
+
+

Checks if an item is in the session and is also not None. +Returns True if both conditions are fulfilled.

+
+ +Expand source code + +
def hasvalue( self, key ):
+    """
+    Checks if an item is in the session and is also not None.
+    Returns True if both conditions are fulfilled.
+    """
+    exists = self.exists( key )
+    not_none = self.get( key, None ) is not None
+    verdict = exists and not_none
+    return verdict
+
+
+
+def remove(self, key) +
+
+

Remove an item from the session

+
+ +Expand source code + +
def remove( self, key ):
+    """
+    Remove an item from the session
+    """
+    try: 
+        st.session_state.pop( key )
+        self._to_export.remove( key )
+    except: 
+        pass
+
+
+
+def set(self, key, value, export=True) +
+
+

A synonym of add

+
+ +Expand source code + +
def set( self, key, value, export = True ):
+    """
+    A synonym of `add`
+    """
+    self.add( key, value, export )
+
+
+
+def setup(self, key, value) +
+
+

Stores an item to the session only if it's not already present.

+
+ +Expand source code + +
def setup( self, key, value ):
+    """
+    Stores an item to the session only if it's not already present.
+    """
+    if not self.exists( key ):
+        self.add( key, value )
+
+
+
+
+
+class stEEGData +(*args, **kwargs) +
+
+

A streamlit compatible version of EEGData +The primary difference here is that it replaces the original filepath variables +with filepath.name arguments to work with the string and not the UploadedFile objects.

+
+ +Expand source code + +
class stEEGData( EEGData ):
+    """
+    A streamlit compatible version of EEGData
+    The primary difference here is that it replaces the original filepath variables
+    with filepath.name arguments to work with the string and not the UploadedFile objects.
+    """
+    def __init__( self, *args, **kwargs ):
+        super().__init__(*args, **kwargs)
+        
+    def _filesuffix(self, filepath):
+        """
+        Returns the suffix from a filepath
+        """
+        suffix = os.path.basename( filepath.name )
+        suffix = suffix.split(".")[-1]
+        return suffix
+
+    def _csv_delimiter(self, filepath):
+        """
+        Checks if a csv file is , or ; delimited and returns the 
+        correct delmiter to use...
+        """
+        # open the file and read 
+        content = deepcopy(filepath).read().decode()
+
+        # check if a semicolon is present
+        # if so, we delimit at ; 
+        has_semicolon = ";" in content
+        delimiter = ";" if has_semicolon else ","
+
+        return delimiter
+    
+    def _check_sanity(self, signal_path, event_path, sampling_frequency):
+        """
+        Checks if valid data inputs were provided
+        """
+
+        # check if the datafiles conform to suppored filetypes
+        fname = os.path.basename(signal_path.name)
+        if not any( [ fname.endswith(suffix) for suffix in supported_filetypes ] ):
+            suffix = fname.split(".")[-1]
+            raise TypeError( f"The signal datafile could not be interpreted ('.{suffix}'), only {supported_filetypes} files are supported!" )
+
+        fname = os.path.basename(event_path.name)
+        if not any( [ fname.endswith(suffix) for suffix in supported_filetypes ] ):
+            suffix = fname.split(".")[-1]
+            raise TypeError( f"The event datafile could not be interpreted ('.{suffix}'), only {supported_filetypes} files are supported!" )
+        
+        # check if the frequency is a positive number
+        if not sampling_frequency > 0:
+            raise ValueError( f"Sampling frequency {sampling_frequency} must be a positive value" )
+
+

Ancestors

+ +

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/EEGToolkit/auxiliary/auxiliary.html b/docs/EEGToolkit/auxiliary/auxiliary.html new file mode 100644 index 0000000..ab5090d --- /dev/null +++ b/docs/EEGToolkit/auxiliary/auxiliary.html @@ -0,0 +1,613 @@ + + + + + + +EEGToolkit.auxiliary.auxiliary API documentation + + + + + + + + + + + +
+
+
+

Module EEGToolkit.auxiliary.auxiliary

+
+
+

Auxiliary functions to work within the streamlit environment

+
+ +Expand source code + +
"""
+Auxiliary functions to work within the streamlit environment
+"""
+
+from ..EEGData import EEGData, supported_filetypes
+import streamlit as st 
+from copy import deepcopy
+import os 
+
+class Session:
+    """
+    A class to handle storing values into and getting them from the streamlit session state.
+    """
+    def __init__(self):
+
+        # a list of keys to include in the 
+        # export session dictionary
+        self._to_export = set()
+
+    def add( self, key, value, export = True ):
+        """
+        Add a new item to the session but 
+        can also change an existing item.
+
+        Parameters
+        ----------
+        key : str
+            The key of the new item
+        value 
+            The value to store of the new item
+        export : bool
+            Wether or not to include the item in the exported session dictioanry.
+        """
+        st.session_state[ key ] = value
+
+        if export: 
+            self._to_export.add( key )
+    
+    def set( self, key, value, export = True ):
+        """
+        A synonym of `add`
+        """
+        self.add( key, value, export )
+
+    def remove( self, key ):
+        """
+        Remove an item from the session
+        """
+        try: 
+            st.session_state.pop( key )
+            self._to_export.remove( key )
+        except: 
+            pass
+
+    def get( self, key, default = None, rm = False, copy = False ):
+        """
+        Tries to get an item from the session 
+        and returns the default if this fails. 
+        If rm = True is set, the item will be 
+        removed after getting. 
+        If copy = True is set, it will return a deepcopy.
+        """
+        try: 
+            value = st.session_state[ key ]
+            if copy: 
+                value = deepcopy(value)
+        except: 
+            value = default
+
+        if rm: 
+            self.remove( key )
+
+        return value 
+
+    def setup( self, key, value ):
+        """
+        Stores an item to the session only if it's not already present.
+        """
+        if not self.exists( key ):
+            self.add( key, value )
+
+    def exists( self, key ):
+        """
+        Checks if an item is in the session. Returns True if so.
+        """
+        verdict = key in st.session_state
+        return verdict
+    
+    def hasvalue( self, key ):
+        """
+        Checks if an item is in the session and is also not None.
+        Returns True if both conditions are fulfilled.
+        """
+        exists = self.exists( key )
+        not_none = self.get( key, None ) is not None
+        verdict = exists and not_none
+        return verdict
+
+    def export( self ):
+        """
+        Exports a copy of the session state for better readablity wherein non-intelligible entries are removed. 
+        """
+        to_export = st.session_state[ self._to_export ]
+        return to_export
+
+
+
+class stEEGData( EEGData ):
+    """
+    A streamlit compatible version of EEGData
+    The primary difference here is that it replaces the original filepath variables
+    with filepath.name arguments to work with the string and not the UploadedFile objects.
+    """
+    def __init__( self, *args, **kwargs ):
+        super().__init__(*args, **kwargs)
+        
+    def _filesuffix(self, filepath):
+        """
+        Returns the suffix from a filepath
+        """
+        suffix = os.path.basename( filepath.name )
+        suffix = suffix.split(".")[-1]
+        return suffix
+
+    def _csv_delimiter(self, filepath):
+        """
+        Checks if a csv file is , or ; delimited and returns the 
+        correct delmiter to use...
+        """
+        # open the file and read 
+        content = deepcopy(filepath).read().decode()
+
+        # check if a semicolon is present
+        # if so, we delimit at ; 
+        has_semicolon = ";" in content
+        delimiter = ";" if has_semicolon else ","
+
+        return delimiter
+    
+    def _check_sanity(self, signal_path, event_path, sampling_frequency):
+        """
+        Checks if valid data inputs were provided
+        """
+
+        # check if the datafiles conform to suppored filetypes
+        fname = os.path.basename(signal_path.name)
+        if not any( [ fname.endswith(suffix) for suffix in supported_filetypes ] ):
+            suffix = fname.split(".")[-1]
+            raise TypeError( f"The signal datafile could not be interpreted ('.{suffix}'), only {supported_filetypes} files are supported!" )
+
+        fname = os.path.basename(event_path.name)
+        if not any( [ fname.endswith(suffix) for suffix in supported_filetypes ] ):
+            suffix = fname.split(".")[-1]
+            raise TypeError( f"The event datafile could not be interpreted ('.{suffix}'), only {supported_filetypes} files are supported!" )
+        
+        # check if the frequency is a positive number
+        if not sampling_frequency > 0:
+            raise ValueError( f"Sampling frequency {sampling_frequency} must be a positive value" )
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Session +
+
+

A class to handle storing values into and getting them from the streamlit session state.

+
+ +Expand source code + +
class Session:
+    """
+    A class to handle storing values into and getting them from the streamlit session state.
+    """
+    def __init__(self):
+
+        # a list of keys to include in the 
+        # export session dictionary
+        self._to_export = set()
+
+    def add( self, key, value, export = True ):
+        """
+        Add a new item to the session but 
+        can also change an existing item.
+
+        Parameters
+        ----------
+        key : str
+            The key of the new item
+        value 
+            The value to store of the new item
+        export : bool
+            Wether or not to include the item in the exported session dictioanry.
+        """
+        st.session_state[ key ] = value
+
+        if export: 
+            self._to_export.add( key )
+    
+    def set( self, key, value, export = True ):
+        """
+        A synonym of `add`
+        """
+        self.add( key, value, export )
+
+    def remove( self, key ):
+        """
+        Remove an item from the session
+        """
+        try: 
+            st.session_state.pop( key )
+            self._to_export.remove( key )
+        except: 
+            pass
+
+    def get( self, key, default = None, rm = False, copy = False ):
+        """
+        Tries to get an item from the session 
+        and returns the default if this fails. 
+        If rm = True is set, the item will be 
+        removed after getting. 
+        If copy = True is set, it will return a deepcopy.
+        """
+        try: 
+            value = st.session_state[ key ]
+            if copy: 
+                value = deepcopy(value)
+        except: 
+            value = default
+
+        if rm: 
+            self.remove( key )
+
+        return value 
+
+    def setup( self, key, value ):
+        """
+        Stores an item to the session only if it's not already present.
+        """
+        if not self.exists( key ):
+            self.add( key, value )
+
+    def exists( self, key ):
+        """
+        Checks if an item is in the session. Returns True if so.
+        """
+        verdict = key in st.session_state
+        return verdict
+    
+    def hasvalue( self, key ):
+        """
+        Checks if an item is in the session and is also not None.
+        Returns True if both conditions are fulfilled.
+        """
+        exists = self.exists( key )
+        not_none = self.get( key, None ) is not None
+        verdict = exists and not_none
+        return verdict
+
+    def export( self ):
+        """
+        Exports a copy of the session state for better readablity wherein non-intelligible entries are removed. 
+        """
+        to_export = st.session_state[ self._to_export ]
+        return to_export
+
+

Methods

+
+
+def add(self, key, value, export=True) +
+
+

Add a new item to the session but +can also change an existing item.

+

Parameters

+
+
key : str
+
The key of the new item
+
value
+
The value to store of the new item
+
export : bool
+
Wether or not to include the item in the exported session dictioanry.
+
+
+ +Expand source code + +
def add( self, key, value, export = True ):
+    """
+    Add a new item to the session but 
+    can also change an existing item.
+
+    Parameters
+    ----------
+    key : str
+        The key of the new item
+    value 
+        The value to store of the new item
+    export : bool
+        Wether or not to include the item in the exported session dictioanry.
+    """
+    st.session_state[ key ] = value
+
+    if export: 
+        self._to_export.add( key )
+
+
+
+def exists(self, key) +
+
+

Checks if an item is in the session. Returns True if so.

+
+ +Expand source code + +
def exists( self, key ):
+    """
+    Checks if an item is in the session. Returns True if so.
+    """
+    verdict = key in st.session_state
+    return verdict
+
+
+
+def export(self) +
+
+

Exports a copy of the session state for better readablity wherein non-intelligible entries are removed.

+
+ +Expand source code + +
def export( self ):
+    """
+    Exports a copy of the session state for better readablity wherein non-intelligible entries are removed. 
+    """
+    to_export = st.session_state[ self._to_export ]
+    return to_export
+
+
+
+def get(self, key, default=None, rm=False, copy=False) +
+
+

Tries to get an item from the session +and returns the default if this fails. +If rm = True is set, the item will be +removed after getting. +If copy = True is set, it will return a deepcopy.

+
+ +Expand source code + +
def get( self, key, default = None, rm = False, copy = False ):
+    """
+    Tries to get an item from the session 
+    and returns the default if this fails. 
+    If rm = True is set, the item will be 
+    removed after getting. 
+    If copy = True is set, it will return a deepcopy.
+    """
+    try: 
+        value = st.session_state[ key ]
+        if copy: 
+            value = deepcopy(value)
+    except: 
+        value = default
+
+    if rm: 
+        self.remove( key )
+
+    return value 
+
+
+
+def hasvalue(self, key) +
+
+

Checks if an item is in the session and is also not None. +Returns True if both conditions are fulfilled.

+
+ +Expand source code + +
def hasvalue( self, key ):
+    """
+    Checks if an item is in the session and is also not None.
+    Returns True if both conditions are fulfilled.
+    """
+    exists = self.exists( key )
+    not_none = self.get( key, None ) is not None
+    verdict = exists and not_none
+    return verdict
+
+
+
+def remove(self, key) +
+
+

Remove an item from the session

+
+ +Expand source code + +
def remove( self, key ):
+    """
+    Remove an item from the session
+    """
+    try: 
+        st.session_state.pop( key )
+        self._to_export.remove( key )
+    except: 
+        pass
+
+
+
+def set(self, key, value, export=True) +
+
+

A synonym of add

+
+ +Expand source code + +
def set( self, key, value, export = True ):
+    """
+    A synonym of `add`
+    """
+    self.add( key, value, export )
+
+
+
+def setup(self, key, value) +
+
+

Stores an item to the session only if it's not already present.

+
+ +Expand source code + +
def setup( self, key, value ):
+    """
+    Stores an item to the session only if it's not already present.
+    """
+    if not self.exists( key ):
+        self.add( key, value )
+
+
+
+
+
+class stEEGData +(*args, **kwargs) +
+
+

A streamlit compatible version of EEGData +The primary difference here is that it replaces the original filepath variables +with filepath.name arguments to work with the string and not the UploadedFile objects.

+
+ +Expand source code + +
class stEEGData( EEGData ):
+    """
+    A streamlit compatible version of EEGData
+    The primary difference here is that it replaces the original filepath variables
+    with filepath.name arguments to work with the string and not the UploadedFile objects.
+    """
+    def __init__( self, *args, **kwargs ):
+        super().__init__(*args, **kwargs)
+        
+    def _filesuffix(self, filepath):
+        """
+        Returns the suffix from a filepath
+        """
+        suffix = os.path.basename( filepath.name )
+        suffix = suffix.split(".")[-1]
+        return suffix
+
+    def _csv_delimiter(self, filepath):
+        """
+        Checks if a csv file is , or ; delimited and returns the 
+        correct delmiter to use...
+        """
+        # open the file and read 
+        content = deepcopy(filepath).read().decode()
+
+        # check if a semicolon is present
+        # if so, we delimit at ; 
+        has_semicolon = ";" in content
+        delimiter = ";" if has_semicolon else ","
+
+        return delimiter
+    
+    def _check_sanity(self, signal_path, event_path, sampling_frequency):
+        """
+        Checks if valid data inputs were provided
+        """
+
+        # check if the datafiles conform to suppored filetypes
+        fname = os.path.basename(signal_path.name)
+        if not any( [ fname.endswith(suffix) for suffix in supported_filetypes ] ):
+            suffix = fname.split(".")[-1]
+            raise TypeError( f"The signal datafile could not be interpreted ('.{suffix}'), only {supported_filetypes} files are supported!" )
+
+        fname = os.path.basename(event_path.name)
+        if not any( [ fname.endswith(suffix) for suffix in supported_filetypes ] ):
+            suffix = fname.split(".")[-1]
+            raise TypeError( f"The event datafile could not be interpreted ('.{suffix}'), only {supported_filetypes} files are supported!" )
+        
+        # check if the frequency is a positive number
+        if not sampling_frequency > 0:
+            raise ValueError( f"Sampling frequency {sampling_frequency} must be a positive value" )
+
+

Ancestors

+ +

Inherited members

+ +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/EEGToolkit/auxiliary/index.html b/docs/EEGToolkit/auxiliary/index.html new file mode 100644 index 0000000..302c5c6 --- /dev/null +++ b/docs/EEGToolkit/auxiliary/index.html @@ -0,0 +1,71 @@ + + + + + + +EEGToolkit.auxiliary API documentation + + + + + + + + + + + +
+
+
+

Module EEGToolkit.auxiliary

+
+
+
+ +Expand source code + +
from .auxiliary import *
+
+
+
+

Sub-modules

+
+
EEGToolkit.auxiliary.auxiliary
+
+

Auxiliary functions to work within the streamlit environment

+
+
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/EEGToolkit/index.html b/docs/EEGToolkit/index.html index 1c01d39..e7728e6 100644 --- a/docs/EEGToolkit/index.html +++ b/docs/EEGToolkit/index.html @@ -38,7 +38,7 @@

Package EEGToolkit

Example Usage

To use this module for data analysis, only three steps are necessary, -(1st) setup of the EEGToolkit.EEGData object, (2nd) event data extraction, and (3rd) +(1st) setup of the EEGToolkit.EEGData object, (2nd) event data extraction, and (3rd) data summary (which performs signal comparison).

# setting up the EEGData with some datafiles
 eeg = EEGData( eeg_path = "data/eeg.npy", event_path = "data/events.npy", sampling_frequency = 500 )
@@ -108,14 +108,22 @@ 

CLI

Sub-modules

-
EEGToolkit.EEGData
+
EEGToolkit.EEGData
-

This module provides a data class EEGToolkit.EEGData to work with EEG signal data for event-reaction-time delay experiments. -It works with two separate input …

+
-
EEGToolkit.EEGStats
+
EEGToolkit.EEGStats
-

Computes time-point-wise the mean and SEM values of EEG data stored in numpy ndarrays.

+
+
+
EEGToolkit.auxiliary
+
+
+
+
EEGToolkit.main
+
+

This script defines an interactive +web-app for the EEGData package.

@@ -137,8 +145,10 @@

Index

diff --git a/docs/EEGToolkit/main.html b/docs/EEGToolkit/main.html new file mode 100644 index 0000000..25ff5ed --- /dev/null +++ b/docs/EEGToolkit/main.html @@ -0,0 +1,213 @@ + + + + + + +EEGToolkit.main API documentation + + + + + + + + + + + +
+
+
+

Module EEGToolkit.main

+
+
+

This script defines an interactive +web-app for the EEGData package.

+
+ +Expand source code + +
"""
+This script defines an interactive 
+web-app for the EEGData package.
+"""
+
+import streamlit as st 
+from EEGToolkit.EEGData import supported_filetypes
+import pandas as pd
+from EEGToolkit.auxiliary import Session, stEEGData
+import os
+
+session = Session()
+
+# =================================================================
+#                          Header Section
+# =================================================================
+
+
+st.markdown( 
+                """
+# EEGToolKit Viewer 
+                """, 
+        )
+
+# =================================================================
+#                        Data Input Section
+# =================================================================
+
+file_container = st.container()
+signal_col, events_col = file_container.columns( 2 )
+
+# add a signal datafile
+signal_file = signal_col.file_uploader( 
+                                        "EEG Signal Data",
+                                        help = f"Upload here a datafile specifying a 1D array of EEG signal data.",
+                                        type = supported_filetypes
+                                )
+if signal_file is not None: 
+        session.add( "signal_filename", signal_file.name )
+        session.add( "signal_file", signal_file, export = False )
+
+# add an events metadata file
+events_file = events_col.file_uploader(
+                                        "Events Metadata",
+                                        help = f"Upload here a datafile specifying a 2-column array of events metadata for the signal data file.",
+                                        type = supported_filetypes
+                                )
+if events_file is not None:
+        session.add( "events_filename", events_file.name )
+        session.add( "events_file", events_file, export = False )
+
+
+# =================================================================
+#                      Data Processing Section
+# =================================================================
+
+# In case anybody is wondering why the weird column setup here. The answer
+# is that streamlit does not (yet) support nested columns nor stretching objects
+# over multiple columns. Hence, one column, one object, if they should "appear" 
+# in side by side, one above the other etc. layouts we need to have multiple
+# column blocks...
+
+controls = st.container()
+upper_ctrl_col1, upper_ctrl_col2 = controls.columns( (1, 3) )
+mid_ctrl_col1, mid_ctrl_col2, mid_ctrl_col3 = controls.columns( (2, 3, 3 ) )
+lower_ctrl_col1, lower_ctrl_col2, lower_ctrl_col3 = controls.columns( (2, 3, 3 ) )
+
+compute_baseline = upper_ctrl_col2.checkbox( 
+                                                "Compare Baseline",
+                                                help = "Compute the baseline of a given signal type / event and compares the signal to the baseline through positition-wise T-Tests.",
+                                                value = True
+                                )
+
+significance_level = upper_ctrl_col2.number_input( 
+                                        "Significance Level", 
+                                        help = "Choose a significance threshold level for signal-signal and signal-baseline comparisons",
+                                        min_value = 1e-5, max_value = 1.0, value = 0.05, step = 1e-3, format = "%e"
+                                )
+session.add( "significance_level", significance_level )
+
+frequency = upper_ctrl_col2.number_input( 
+                                        "Sampling Frequency", 
+                                        help = "The sampling frequency in `Hertz` at which the signal was recorded",
+                                        min_value = 1, max_value = 100000, value = 500, step = 100, format = "%d", 
+                                )
+session.add( "frequency", frequency )
+
+
+start_sec = mid_ctrl_col2.number_input( 
+                                        "Upstream Buffer", 
+                                        help = "The time buffer pre-event to extract (in seconds).",
+                                        min_value = 1e-4, max_value = 10.0, value = 0.01, step = 0.001, format = "%e", 
+                                )
+session.add( "upstream_buffer", start_sec )
+
+stop_sec = mid_ctrl_col3.number_input( 
+                                        "Downstream Buffer", 
+                                        help = "The time buffer post-event to extract (in seconds).",
+                                        min_value = 1e-4, max_value = 10.0, value = 1.0, step = 0.001, format = "%e", 
+                                )
+session.add( "downstream_buffer", stop_sec )
+
+xscale = lower_ctrl_col2.number_input( 
+                                        "Timeframe Scale", 
+                                        help = "A scaling factor to adjust x-axis time scale. E.g. `1000` to adjust data from seconds to milliseconds.",
+                                        min_value = 1, max_value = 100000, value = 1000, step = 100, format = "%d", 
+                                )
+session.add( "timescale", xscale )
+
+yscale = lower_ctrl_col3.number_input( 
+                                        "Signal Scale", 
+                                        help = "A scaling factor to adjust y-axis signal scale. E.g. `1000` to adjust from volts to millivolts.",
+                                        min_value = 1, max_value = 100000, value = 1000, step = 100, format = "%d", 
+                                )
+session.add( "signalscale", yscale )
+
+# slightly ugly splitting of the text, but the only way to adjust the text to the column layout I want...
+mid_ctrl_col1.markdown( "Once your setup is done, press the `Compute` button below to " )
+lower_ctrl_col1.markdown( "commence data evaluation." )
+run_button = lower_ctrl_col1.button( 
+                                        "Compute",
+                                        help = "Perform computations and output a figure",
+                                )
+
+# =================================================================
+#                       Computational Section
+# =================================================================
+
+figure_container = st.container()
+if run_button:
+        if signal_file is None: 
+                st.error( "No Signal File was provided so far!" )
+                st.stop()
+        if events_file is None:
+                st.error( "No Events File was provided so far!" )
+                st.stop()
+        eegdata = stEEGData( 
+                                signal_path = signal_file, 
+                                event_path = events_file, 
+                                sampling_frequency = frequency 
+                        )
+        eegdata.extract( -start_sec, stop_sec )
+
+        if compute_baseline:
+                eegdata.baseline()
+        fig = eegdata.summary( 
+                                x_scale = xscale, 
+                                y_scale = yscale, 
+                                significance_level = significance_level 
+                        )
+        
+        figure_container.pyplot( fig )
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b87f6ea..f3eeb4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ matplotlib==3.5.0 numpy==1.20.3 pandas==1.3.4 statsmodels==0.12.2 +streamlit==1.7.0 diff --git a/setup.py b/setup.py index 2a32f13..4cd8ce8 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="EEGToolkit", - version="1.0.0", + version="2.0.0", author="Axel Giottonini, Noah Kleinschmidt, Kalvin Dobler", author_email="axel.giottonini@unifr.ch, noah.kleinschmidt@students.unibe.ch, kalvin.dobler@unifr.ch", description="A package for EEG data analysis of reaction time-delay experiments.", @@ -13,6 +13,11 @@ long_description_content_type="text/markdown", url="https://github.com/AxelGiottonini/AP-EEG.git", packages=setuptools.find_packages(), + entry_points ={ + "console_scripts": [ + "EEGToolkit = EEGToolkit.EEGData:main" + ] + }, classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", @@ -20,5 +25,5 @@ "Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Scientific/Engineering :: Visualization" ], - python_requires='>=3.6', + python_requires=">=3.6", ) \ No newline at end of file