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.

Debug screen added; blocked by pyodide error

authored by

jdpigeon and committed by
Teon L Brooks
996249bc e76d27c5

+119 -49
+24
app/components/HomeComponent/index.tsx
··· 39 39 import EEGExplorationComponent from '../EEGExplorationComponent'; 40 40 import { SignalQualityData } from '../../constants/interfaces'; 41 41 import { getExperimentFromType } from '../../utils/labjs/functions'; 42 + import PyodidePlotWidget from '../PyodidePlotWidget'; 42 43 43 44 const { dialog } = remote; 44 45 ··· 47 48 RECENT: 'MY EXPERIMENTS', 48 49 NEW: 'EXPERIMENT BANK', 49 50 EXPLORE: 'EXPLORE EEG DATA', 51 + PYODIDE_TEST: 'PYODIDE_TEST', 50 52 }; 51 53 52 54 export interface Props { ··· 61 63 history: History; 62 64 PyodideActions: typeof PyodideActions; 63 65 signalQualityObservable?: Observable<SignalQualityData>; 66 + topoPlot: { 67 + [key: string]: string; 68 + }; 64 69 } 65 70 66 71 interface State { ··· 359 364 availableDevices={this.props.availableDevices} 360 365 DeviceActions={this.props.DeviceActions} 361 366 /> 367 + ); 368 + case HOME_STEPS.PYODIDE_TEST: 369 + return ( 370 + <Grid columns="two" relaxed padded> 371 + <Grid.Row> 372 + <Grid.Column> 373 + <Button onClick={this.props.PyodideActions.LoadTopo}> 374 + Generate Plot 375 + </Button> 376 + </Grid.Column> 377 + <Grid.Column> 378 + <PyodidePlotWidget 379 + title={"Test Plot"} 380 + imageTitle={`Test-Topoplot`} 381 + plotMIMEBundle={this.props.topoPlot} 382 + /> 383 + </Grid.Column> 384 + </Grid.Row> 385 + </Grid> 362 386 ); 363 387 } 364 388 }
+1
app/containers/HomeContainer.ts
··· 6 6 function mapStateToProps(state) { 7 7 return { 8 8 ...state.device, 9 + ...state.pyodide, 9 10 }; 10 11 } 11 12
+32 -19
app/epics/pyodideEpics.ts
··· 17 17 plotPSD, 18 18 plotERP, 19 19 plotTopoMap, 20 + plotTestPlot, 20 21 saveEpochs, 21 22 loadPyodide, 23 + loadUtils, 22 24 } from '../utils/pyodide'; 23 25 import { 24 26 EMOTIV_CHANNELS, ··· 39 41 action$.pipe( 40 42 filter(isActionOf(PyodideActions.Launch)), 41 43 tap(() => console.log('launching')), 42 - map(loadPyodide), 44 + mergeMap(loadPyodide), 45 + tap((worker) => { 46 + console.log('loadPyodide completed, laoding utils'); 47 + // loadUtils(worker); 48 + }), 43 49 map(PyodideActions.SetPyodideWorker) 44 50 ); 45 51 ··· 81 87 const { results, error } = e.data; 82 88 83 89 if (results && !error) { 84 - toast(`Pyodide: `, results); 90 + toast.error(`Pyodide: ${results}`); 85 91 } else if (error) { 86 - toast.error('Pyodide: ', error); 92 + toast.error(`Pyodide: ${error}`); 87 93 } 88 94 }), 89 95 map(PyodideActions.ReceiveMessage) ··· 98 104 pluck('payload'), 99 105 filter((filePathsArray: string[]) => filePathsArray.length >= 1), 100 106 map((filePathsArray) => readFiles(filePathsArray)), 101 - mergeMap((csvArray) => loadCSV(csvArray)), 102 - mergeMap(() => filterIIR(1, 30)), 107 + mergeMap((csvArray) => loadCSV(state$.value.pyodide.worker!, csvArray)), 108 + mergeMap(() => filterIIR(state$.value.pyodide.worker!, 1, 30)), 103 109 map(() => { 104 110 if (!state$.value.experiment.params?.stimuli) { 105 111 return {}; 106 112 } 107 113 108 114 return epochEvents( 115 + state$.value.pyodide.worker!, 109 116 Object.fromEntries( 110 117 state$.value.experiment.params?.stimuli.map((stimulus, i) => [ 111 118 stimulus.title, ··· 126 133 PyodideActionType, 127 134 PyodideActionType, 128 135 RootState 129 - > = (action$) => 136 + > = (action$, state$) => 130 137 action$.pipe( 131 138 filter(isActionOf(PyodideActions.LoadCleanedEpochs)), 132 139 pluck('payload'), 133 140 filter((filePathsArray) => filePathsArray.length >= 1), 134 - map(loadCleanedEpochs), 141 + map((epochsArray) => 142 + loadCleanedEpochs(state$.value.pyodide.worker!, epochsArray) 143 + ), 135 144 mergeMap(() => 136 145 of( 137 146 PyodideActions.GetEpochsInfo(PYODIDE_VARIABLE_NAMES.CLEAN_EPOCHS), ··· 147 156 ) => 148 157 action$.pipe( 149 158 filter(isActionOf(PyodideActions.CleanEpochs)), 150 - mergeMap(cleanEpochsPlot), 159 + mergeMap(() => cleanEpochsPlot(state$.value.pyodide.worker!)), 151 160 map(() => 152 161 saveEpochs( 162 + state$.value.pyodide.worker!, 153 163 getWorkspaceDir(state$.value.experiment.title), 154 164 state$.value.experiment.subject 155 165 ) ··· 165 175 action$.pipe( 166 176 filter(isActionOf(PyodideActions.GetEpochsInfo)), 167 177 pluck('payload'), 168 - mergeMap(requestEpochsInfo), 178 + mergeMap((varName) => 179 + requestEpochsInfo(state$.value.pyodide.worker!, varName) 180 + ), 169 181 map((epochInfoArray) => 170 182 epochInfoArray.map((infoObj) => ({ 171 183 name: Object.keys(infoObj)[0], ··· 182 194 > = (action$, state$) => 183 195 action$.pipe( 184 196 filter(isActionOf(PyodideActions.GetChannelInfo)), 185 - mergeMap(requestChannelInfo), 197 + mergeMap(() => requestChannelInfo(state$.value.pyodide.worker!)), 186 198 map((channelInfoString) => 187 199 PyodideActions.SetChannelInfo(parseSingleQuoteJSON(channelInfoString)) 188 200 ) ··· 194 206 ) => 195 207 action$.pipe( 196 208 filter(isActionOf(PyodideActions.LoadPSD)), 197 - mergeMap(plotPSD), 209 + mergeMap(() => plotPSD(state$.value.pyodide.worker!)), 198 210 map(PyodideActions.SetPSDPlot) 199 211 ); 200 212 ··· 204 216 ) => 205 217 action$.pipe( 206 218 filter(isActionOf(PyodideActions.LoadTopo)), 207 - mergeMap(plotTopoMap), 219 + // mergeMap(plotTopoMap), 220 + mergeMap(() => plotTestPlot(state$.value.pyodide.worker!)), 208 221 tap((e) => console.log('received topo map: ', e)), 209 222 mergeMap((topoPlot) => 210 223 of( 211 - PyodideActions.SetTopoPlot(topoPlot), 212 - PyodideActions.LoadERP( 213 - state$.value.device.deviceType === DEVICES.EMOTIV 214 - ? EMOTIV_CHANNELS[0] 215 - : MUSE_CHANNELS[0] 216 - ) 224 + PyodideActions.SetTopoPlot(topoPlot) 225 + // PyodideActions.LoadERP( 226 + // state$.value.device.deviceType === DEVICES.EMOTIV 227 + // ? EMOTIV_CHANNELS[0] 228 + // : MUSE_CHANNELS[0] 229 + // ) 217 230 ) 218 231 ) 219 232 ); ··· 241 254 ); 242 255 return parseInt(EMOTIV_CHANNELS[0], 10); 243 256 }), 244 - mergeMap(plotERP), 257 + mergeMap((chanIndex) => plotERP(state$.value.pyodide.worker!, chanIndex)), 245 258 map(PyodideActions.SetERPPlot) 246 259 ); 247 260
+9 -1
app/reducers/pyodideReducer.ts
··· 24 24 } 25 25 | null 26 26 | undefined; 27 + readonly worker: Worker | null; 27 28 } 28 29 29 - const initialState = { 30 + const initialState: PyodideStateType = { 30 31 epochsInfo: [], 31 32 channelInfo: [], 32 33 psdPlot: null, 33 34 topoPlot: null, 34 35 erpPlot: null, 36 + worker: null, 35 37 }; 36 38 37 39 export default createReducer(initialState, (builder) => 38 40 builder 41 + .addCase(PyodideActions.SetPyodideWorker, (state, action) => { 42 + return { 43 + ...state, 44 + worker: action.payload, 45 + }; 46 + }) 39 47 .addCase(PyodideActions.SetEpochInfo, (state, action) => { 40 48 return { 41 49 ...state,
+51 -27
app/utils/pyodide/index.ts
··· 2 2 import { readFileSync } from 'fs'; 3 3 import { formatFilePath } from './functions'; 4 4 5 - declare const pyodideWorker: Worker; 6 - 7 5 // --------------------------------- 8 6 // This file contains the JS functions that allow the app to access python-wasm through pyodide 9 7 // These functions wrap the python strings defined in the ··· 11 9 // ----------------------------- 12 10 // Imports and Utility functions 13 11 14 - export const loadPyodide = () => { 15 - return new Worker('./utils/pyodide/webworker.js'); 12 + export const loadPyodide = async () => { 13 + const freshWorker = await new Worker('./utils/pyodide/webworker.js'); 14 + return freshWorker; 16 15 }; 17 16 18 - export const loadUtils = async () => 19 - pyodideWorker.postMessage({ 17 + export const loadUtils = async (worker: Worker) => 18 + worker.postMessage({ 20 19 data: readFileSync(path.join(__dirname, '/utils/pyodide/utils.py'), 'utf8'), 21 20 }); 22 21 23 - export const loadCSV = async (csvArray: Array<any>) => { 22 + export const loadCSV = async (worker: Worker, csvArray: Array<any>) => { 24 23 // TODO: Pass attached variable name as parameter to load_data 25 24 // @ts-expect-error 26 25 window.csvArray = csvArray; 27 - await pyodideWorker.postMessage({ data: `raw = load_data()` }); 26 + await worker.postMessage({ data: `raw = load_data()` }); 28 27 }; 29 28 30 29 // --------------------------- 31 30 // MNE-Related Data Processing 32 31 33 - export const loadCleanedEpochs = async (epochsArray: string[]) => { 34 - await pyodideWorker.postMessage({ 32 + export const loadCleanedEpochs = async ( 33 + worker: Worker, 34 + epochsArray: string[] 35 + ) => { 36 + await worker.postMessage({ 35 37 data: [ 36 38 `clean_epochs = concatenate_epochs([read_epochs(file) for file in ${epochsArray}])`, 37 39 `conditions = OrderedDict({key: [value] for (key, value) in clean_epochs.event_id.items()})`, ··· 40 42 }; 41 43 42 44 // NOTE: this command includes a ';' to prevent returning data 43 - export const filterIIR = async (lowCutoff: number, highCutoff: number) => 44 - pyodideWorker.postMessage({ 45 + export const filterIIR = async ( 46 + worker: Worker, 47 + lowCutoff: number, 48 + highCutoff: number 49 + ) => 50 + worker.postMessage({ 45 51 data: `raw.filter(${lowCutoff}, ${highCutoff}, method='iir');`, 46 52 }); 47 53 48 54 export const epochEvents = async ( 55 + worker: Worker, 49 56 eventIDs: { [k: string]: number }, 50 57 tmin: number, 51 58 tmax: number, 52 59 reject?: string[] | 'None' 53 60 ) => 54 - pyodideWorker.postMessage({ 61 + worker.postMessage({ 55 62 data: [ 56 63 `event_id = ${JSON.stringify(eventIDs)}`, 57 64 `tmin=${tmin}`, ··· 67 74 ].join('\n'), 68 75 }); 69 76 70 - export const requestEpochsInfo = async (variableName: string) => { 71 - const pyodideReturn = await pyodideWorker.postMessage({ 77 + export const requestEpochsInfo = async ( 78 + worker: Worker, 79 + variableName: string 80 + ) => { 81 + const pyodideReturn = await worker.postMessage({ 72 82 data: `get_epochs_info(${variableName})`, 73 83 }); 74 84 return pyodideReturn; 75 85 }; 76 86 77 - export const requestChannelInfo = async () => 78 - pyodideWorker.postMessage({ 87 + export const requestChannelInfo = async (worker: Worker) => 88 + worker.postMessage({ 79 89 data: `[ch for ch in clean_epochs.ch_names if ch != 'Marker']`, 80 90 }); 81 91 82 92 // ----------------------------- 83 93 // Plot functions 84 94 85 - export const cleanEpochsPlot = async () => { 95 + export const cleanEpochsPlot = async (worker: Worker) => { 86 96 // TODO: Figure out how to get image results from pyodide 87 - await pyodideWorker.postMessage({ 97 + await worker.postMessage({ 88 98 data: `raw_epochs.plot(scalings='auto', n_epochs=6, title="Clean Data", events=None)`, 89 99 }); 90 100 }; 91 101 92 - export const plotPSD = async () => { 102 + export const plotPSD = async (worker: Worker) => { 93 103 // TODO: Figure out how to get image results from pyodide 94 - return pyodideWorker.postMessage({ data: `raw.plot_psd(fmin=1, fmax=30)` }); 104 + return worker.postMessage({ data: `raw.plot_psd(fmin=1, fmax=30)` }); 95 105 }; 96 106 97 - export const plotTopoMap = async () => { 107 + export const plotTopoMap = async (worker: Worker) => { 98 108 // TODO: Figure out how to get image results from pyodide 99 - return pyodideWorker.postMessage({ 109 + return worker.postMessage({ 100 110 data: `plot_topo(clean_epochs, conditions)`, 101 111 }); 102 112 }; 103 113 104 - export const plotERP = async (channelIndex: number) => { 105 - return pyodideWorker.postMessage({ 114 + export const plotTestPlot = async (worker: Worker | null) => { 115 + if (!worker) { 116 + return; 117 + } 118 + // TODO: Figure out how to get image results from pyodide 119 + return worker.postMessage({ 120 + data: `plt.plot([1,2,3,4])`, 121 + }); 122 + }; 123 + 124 + export const plotERP = async (worker: Worker, channelIndex: number) => { 125 + return worker.postMessage({ 106 126 data: `X, y = plot_conditions(clean_epochs, ch_ind=${channelIndex}, conditions=conditions, 107 127 ci=97.5, n_boot=1000, title='', diff_waveform=None)`, 108 128 }); 109 129 }; 110 130 111 - export const saveEpochs = (workspaceDir: string, subject: string) => 112 - pyodideWorker.postMessage({ 131 + export const saveEpochs = ( 132 + worker: Worker, 133 + workspaceDir: string, 134 + subject: string 135 + ) => 136 + worker.postMessage({ 113 137 data: `raw_epochs.save(${formatFilePath( 114 138 path.join( 115 139 workspaceDir,
+2 -2
app/utils/pyodide/webworker.js
··· 7 7 importScripts('./src/pyodide/pyodide.js'); 8 8 9 9 async function loadPyodideAndPackages() { 10 - await loadPyodide({ indexURL: './src/pyodide/' }); 11 - await self.pyodide.loadPackage(['matplotlib', 'mne', 'pandas']); 10 + self.pyodide = await loadPyodide({ indexURL: './src/pyodide/' }); 11 + await self.pyodide.loadPackage(['numpy']); 12 12 } 13 13 let pyodideReadyPromise = loadPyodideAndPackages(); 14 14