An easy-to-use platform for EEG experimentation in the classroom
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

WIP cleanup and overhaul

+163 -148
+8 -16
app/actions/pyodideActions.js
··· 1 1 // ------------------------------------------------------------------------- 2 2 // Action Types 3 3 4 - export const LAUNCH_KERNEL = 'LAUNCH_KERNEL'; 5 - export const REQUEST_KERNEL_INFO = 'REQUEST_KERNEL_INFO'; 6 - export const SEND_EXECUTE_REQUEST = 'SEND_EXECUTE_REQUEST'; 7 - export const LOAD_EPOCHS = 'LOAD_EPOCHS'; 8 - export const LOAD_CLEANED_EPOCHS = 'LOAD_CLEANED_EPOCHS'; 9 - export const LOAD_PSD = 'LOAD_PSD'; 10 - export const LOAD_ERP = 'LOAD_ERP'; 11 - export const LOAD_TOPO = 'LOAD_TOPO'; 12 - export const CLEAN_EPOCHS = 'CLEAN_EPOCHS'; 13 - export const CLOSE_KERNEL = 'CLOSE_KERNEL'; 4 + export const SEND_EXECUTE_REQUEST = "SEND_EXECUTE_REQUEST"; 5 + export const LOAD_EPOCHS = "LOAD_EPOCHS"; 6 + export const LOAD_CLEANED_EPOCHS = "LOAD_CLEANED_EPOCHS"; 7 + export const LOAD_PSD = "LOAD_PSD"; 8 + export const LOAD_ERP = "LOAD_ERP"; 9 + export const LOAD_TOPO = "LOAD_TOPO"; 10 + export const CLEAN_EPOCHS = "CLEAN_EPOCHS"; 11 + export const CLOSE_KERNEL = "CLOSE_KERNEL"; 14 12 15 13 // ------------------------------------------------------------------------- 16 14 // Actions 17 - 18 - export const launchKernel = () => ({ type: LAUNCH_KERNEL }); 19 - 20 - export const requestKernelInfo = () => ({ type: REQUEST_KERNEL_INFO }); 21 15 22 16 export const sendExecuteRequest = (payload: string) => ({ 23 17 payload, ··· 48 42 }); 49 43 50 44 export const cleanEpochs = () => ({ type: CLEAN_EPOCHS }); 51 - 52 - export const closeKernel = () => ({ type: CLOSE_KERNEL });
+2 -2
app/constants/constants.ts
··· 50 50 AVAILABLE = 'AVAILABLE', 51 51 } 52 52 53 - // Names of variables in the jupyter kernel 54 - export enum JUPYTER_VARIABLE_NAMES { 53 + // Names of variables in the pyodide kernel 54 + export enum PYODIDE_VARIABLE_NAMES { 55 55 RAW_EPOCHS = 'raw_epochs', 56 56 CLEAN_EPOCHS = 'clean_epochs', 57 57 }
+81
app/constants/interfaces.js
··· 1 + /* 2 + * This file contains all the custom types that we use for Flow type checking 3 + */ 4 + 5 + import { EVENTS } from './constants'; 6 + 7 + // TODO: Write interfaces for device objects (Observables, Classes, etc) 8 + 9 + // ------------------------------------------------------------------ 10 + // lab.js Experiment 11 + 12 + export type ExperimentParameters = { 13 + trialDuration: number, 14 + nbTrials: number, 15 + iti: number, 16 + jitter: number, 17 + sampleType: string, 18 + intro: string, 19 + // Setting this to any prevents ridiculous flow runtime errors 20 + showProgessBar: any, 21 + stimulus1: { dir: string, type: EVENTS, title: string, response: string }, 22 + stimulus2: { dir: string, type: EVENTS, title: string, response: string }, 23 + }; 24 + 25 + export type ExperimentDescription = { 26 + question: string, 27 + hypothesis: string, 28 + methods: string, 29 + }; 30 + 31 + // Array of timeline and trial ids that will be presented in experiment 32 + export type MainTimeline = Array<string>; 33 + 34 + // jsPsych trial presented as part of an experiment 35 + export interface Trial { 36 + id: string; 37 + type: string; 38 + stimulus?: string | StimulusVariable; 39 + trial_duration?: number | (() => number); 40 + post_trial_gap?: number; 41 + on_load?: (string) => void | StimulusVariable; 42 + choices?: Array<string>; 43 + } 44 + 45 + // Timeline of jsPsych trials 46 + export type Timeline = { 47 + id: string, 48 + timeline: Array<Trial>, 49 + sample?: SampleParameter, 50 + timeline_variables?: Array<Object>, 51 + }; 52 + 53 + export interface SampleParameter { 54 + type: string; 55 + size?: number; 56 + fn?: () => Array<number>; 57 + } 58 + 59 + export type StimulusVariable = () => any; 60 + 61 + // -------------------------------------------------------------------- 62 + // Device 63 + 64 + export interface EEGData { 65 + data: Array<number>; 66 + timestamp: number; 67 + marker?: string | number; 68 + } 69 + 70 + export interface DeviceInfo { 71 + name: string; 72 + samplingRate: number; 73 + } 74 + 75 + // -------------------------------------------------------------------- 76 + // General 77 + 78 + export interface ActionType { 79 + +payload: any; 80 + +type: string; 81 + }
+18 -96
app/epics/pyodideEpics.js
··· 1 1 import { combineEpics } from 'redux-observable'; 2 2 import { from, of } from 'rxjs'; 3 - import { map, mergeMap, tap, pluck, ignoreElements, filter, take } from 'rxjs/operators'; 4 - import { find } from 'kernelspecs'; 5 - import { launchSpec } from 'spawnteract'; 6 - import { createMainChannel } from 'enchannel-zmq-backend'; 3 + import { 4 + map, 5 + mergeMap, 6 + tap, 7 + pluck, 8 + ignoreElements, 9 + filter, 10 + take 11 + } from 'rxjs/operators'; 7 12 import { isNil } from 'lodash'; 8 - import { kernelInfoRequest, executeRequest } from '@nteract/messaging'; 9 13 import { toast } from 'react-toastify'; 10 14 import { getWorkspaceDir } from '../utils/filesystem/storage'; 11 15 import { 12 - LAUNCH_KERNEL, 13 - REQUEST_KERNEL_INFO, 14 16 LOAD_EPOCHS, 15 17 LOAD_CLEANED_EPOCHS, 16 18 LOAD_PSD, 17 19 LOAD_ERP, 18 20 LOAD_TOPO, 19 21 CLEAN_EPOCHS, 20 - CLOSE_KERNEL, 21 22 loadTopo, 22 23 loadERP 23 24 } from '../actions/pyodideActions'; ··· 41 42 EVENTS, 42 43 DEVICES, 43 44 MUSE_CHANNELS, 44 - JUPYTER_VARIABLE_NAMES, 45 + PYODIDE_VARIABLE_NAMES 45 46 } from '../constants/constants'; 46 - import { 47 - parseSingleQuoteJSON, 48 - parseKernelStatus, 49 - debugParseMessage 50 - } from '../utils/pyodide/functions'; 47 + 51 48 52 49 export const GET_EPOCHS_INFO = 'GET_EPOCHS_INFO'; 53 50 export const GET_CHANNEL_INFO = 'GET_CHANNEL_INFO'; ··· 140 137 // ------------------------------------------------------------------------- 141 138 // Epics 142 139 143 - const launchEpic = (action$) => 144 - action$.ofType(LAUNCH_KERNEL).pipe( 145 - mergeMap(() => from(find('brainwaves'))), 146 - tap((kernelInfo) => { 147 - if (isNil(kernelInfo)) { 148 - toast.error("Could not find 'brainwaves' jupyter kernel. Have you installed Python?"); 149 - } 150 - }), 151 - filter((kernelInfo) => !isNil(kernelInfo)), 152 - mergeMap((kernelInfo) => 153 - from( 154 - launchSpec(kernelInfo.spec, { 155 - // No STDIN, opt in to STDOUT and STDERR as node streams 156 - stdio: ['ignore', 'pipe', 'pipe'], 157 - }) 158 - ) 159 - ), 160 - tap((kernel) => { 161 - // Route everything that we won't get in messages to our own stdout 162 - kernel.spawn.stdout.on('data', (data) => { 163 - const text = data.toString(); 164 - console.log('KERNEL STDOUT: ', text); 165 - }); 166 - kernel.spawn.stderr.on('data', (data) => { 167 - const text = data.toString(); 168 - console.log('KERNEL STDERR: ', text); 169 - toast.error('Jupyter: ', text); 170 - }); 171 - 172 - kernel.spawn.on('close', () => { 173 - console.log('Kernel closed'); 174 - }); 175 - }), 176 - map(setKernel) 177 - ); 178 - 179 - const setUpChannelEpic = (action$) => 180 - action$.ofType(SET_KERNEL).pipe( 181 - pluck('payload'), 182 - mergeMap((kernel) => from(createMainChannel(kernel.config))), 183 - tap((mainChannel) => mainChannel.next(executeRequest(imports()))), 184 - tap((mainChannel) => mainChannel.next(executeRequest(utils()))), 185 - map(setMainChannel) 186 - ); 187 - 188 - const receiveChannelMessageEpic = (action$, state$) => 189 - action$.ofType(SET_MAIN_CHANNEL).pipe( 190 - mergeMap(() => 191 - state$.value.jupyter.mainChannel.pipe( 192 - map((msg) => { 193 - console.log(debugParseMessage(msg)); 194 - switch (msg['header']['msg_type']) { 195 - case 'kernel_info_reply': 196 - return setKernelInfo(msg); 197 - case 'status': 198 - return setKernelStatus(parseKernelStatus(msg)); 199 - case 'stream': 200 - return receiveStream(msg); 201 - case 'execute_reply': 202 - return receiveExecuteReply(msg); 203 - case 'execute_result': 204 - return receiveExecuteResult(msg); 205 - case 'display_data': 206 - return receiveDisplayData(msg); 207 - default: 208 - } 209 - }), 210 - filter((action) => !isNil(action)) 211 - ) 212 - ) 213 - ); 214 - 215 - const requestKernelInfoEpic = (action$, state$) => 216 - action$.ofType(REQUEST_KERNEL_INFO).pipe( 217 - filter(() => state$.value.jupyter.mainChannel), 218 - map(() => state$.value.jupyter.mainChannel.next(kernelInfoRequest())), 219 - ignoreElements() 220 - ); 221 - 222 140 const loadEpochsEpic = (action$, state$) => 223 141 action$.ofType(LOAD_EPOCHS).pipe( 224 142 pluck('payload'), ··· 246 164 state$.value.jupyter.mainChannel.next(executeRequest(epochEventsCommand)) 247 165 ), 248 166 awaitOkMessage(action$), 249 - map(() => getEpochsInfo(JUPYTER_VARIABLE_NAMES.RAW_EPOCHS)) 167 + map(() => getEpochsInfo(PYODIDE_VARIABLE_NAMES.RAW_EPOCHS)) 250 168 ); 251 169 252 170 const loadCleanedEpochsEpic = (action$, state$) => ··· 258 176 ), 259 177 awaitOkMessage(action$), 260 178 mergeMap(() => 261 - of(getEpochsInfo(JUPYTER_VARIABLE_NAMES.CLEAN_EPOCHS), getChannelInfo(), loadTopo()) 179 + of( 180 + getEpochsInfo(PYODIDE_VARIABLE_NAMES.CLEAN_EPOCHS), 181 + getChannelInfo(), 182 + loadTopo() 183 + ) 262 184 ) 263 185 ); 264 186 ··· 285 207 ) 286 208 ), 287 209 awaitOkMessage(action$), 288 - map(() => getEpochsInfo(JUPYTER_VARIABLE_NAMES.RAW_EPOCHS)) 210 + map(() => getEpochsInfo(PYODIDE_VARIABLE_NAMES.RAW_EPOCHS)) 289 211 ); 290 212 291 213 const getEpochsInfoEpic = (action$, state$) =>
+1 -1
app/utils/pyodide/commands.py
··· 8 8 readFileSync(path.join(__dirname, '/utils/pyodide/pyimport.py'), 'utf8'); 9 9 10 10 export const utils = () => 11 - readFileSync(path.join(__dirname, '/utils/jupyter/utils.py'), 'utf8'); 11 + readFileSync(path.join(__dirname, '/utils/pyodide/utils.py'), 'utf8'); 12 12 13 13 export const loadCSV = (filePathArray: Array<string>) => 14 14 [
+53 -33
app/utils/pyodide/utils.py
··· 14 14 15 15 16 16 def load_data(fnames, sfreq=128., replace_ch_names=None): 17 - """Load CSV files from the /data directory into a Raw object. 18 - 19 - Args: 20 - fnames (array): CSV filepaths from which to load data 17 + """Load CSV files from the /data directory into a RawArray object. 21 18 22 - Keyword Args: 23 - sfreq (float): EEG sampling frequency 24 - replace_ch_names (dict or None): dictionary containing a mapping to 25 - rename channels. Useful when an external electrode was used. 19 + Parameters 20 + ---------- 21 + fnames : list 22 + CSV filepaths from which to load data 23 + sfreq : float 24 + EEG sampling frequency 25 + replace_ch_names : dict | None 26 + A dict containing a mapping to rename channels. 27 + Useful when an external electrode was used during recording. 26 28 27 - Returns: 28 - (mne.io.array.array.RawArray): loaded EEG 29 + Returns 30 + ------- 31 + raw : an instance of mne.io.RawArray 32 + The loaded data. 29 33 """ 30 34 31 35 raw = [] ··· 94 98 return evoked_topo 95 99 96 100 97 - def plot_conditions(epochs, ch_ind=0, conditions=OrderedDict(), ci=97.5, n_boot=1000, 98 - title='', palette=None, 99 - diff_waveform=(4, 3)): 101 + def plot_conditions(epochs, ch_ind=0, conditions=OrderedDict(), ci=97.5, 102 + n_boot=1000, title='', palette=None, diff_waveform=(4, 3)): 100 103 """Plot Averaged Epochs with ERP conditions. 101 104 102 - Args: 103 - epochs (mne.epochs): EEG epochs 105 + Parameters 106 + ---------- 107 + epochs : an instance of mne.epochs 108 + EEG epochs 109 + conditions : an instance of OrderedDict 110 + An ordered dictionary that contains the names of the 111 + conditions to plot as keys, and the list of corresponding marker 112 + numbers as value. 104 113 105 - Keyword Args: 106 - conditions (OrderedDict): dictionary that contains the names of the 107 - conditions to plot as keys, and the list of corresponding marker 108 - numbers as value. E.g., 114 + E.g., 109 115 110 - conditions = {'Non-target': [0, 1], 111 - 'Target': [2, 3, 4]} 116 + conditions = {'Non-target': [0, 1], 117 + 'Target': [2, 3, 4]} 112 118 113 - ch_ind (int): index of channel to plot data from 114 - ci (float): confidence interval in range [0, 100] 115 - n_boot (int): number of bootstrap samples 116 - title (str): title of the figure 117 - palette (list): color palette to use for conditions 118 - ylim (tuple): (ymin, ymax) 119 - diff_waveform (tuple or None): tuple of ints indicating which 120 - conditions to subtract for producing the difference waveform. 119 + ch_ind : int 120 + An index of channel to plot data from. 121 + ci : float 122 + The confidence interval of the measurement within 123 + the range [0, 100]. 124 + n_boot : int 125 + Number of bootstrap samples. 126 + title : str 127 + Title of the figure. 128 + palette : list 129 + Color palette to use for conditions. 130 + ylim : tuple 131 + (ymin, ymax) 132 + diff_waveform : tuple | None 133 + tuple of ints indicating which conditions to subtract for 134 + producing the difference waveform. 121 135 If None, do not plot a difference waveform 122 136 123 - Returns: 124 - (matplotlib.figure.Figure): figure object 125 - (list of matplotlib.axes._subplots.AxesSubplot): list of axes 137 + Returns 138 + ------- 139 + fig : an instance of matplotlib.figure.Figure 140 + A figure object. 141 + ax : list of matplotlib.axes._subplots.AxesSubplot 142 + A list of axes 126 143 """ 127 144 if isinstance(conditions, dict): 128 145 conditions = OrderedDict(conditions) ··· 172 189 return fig, ax 173 190 174 191 def get_epochs_info(epochs): 175 - return [*[{x: len(epochs[x])} for x in epochs.event_id], {"Drop Percentage": round((1 - len(epochs.events)/len(epochs.drop_log)) * 100, 2)}, {"Total Epochs": len(epochs.events)}] 192 + return [*[{x: len(epochs[x])} for x in epochs.event_id], 193 + {"Drop Percentage": round((1 - len(epochs.events) / 194 + len(epochs.drop_log)) * 100, 2)}, 195 + {"Total Epochs": len(epochs.events)}]