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.

Switched to web worker with different epic pattern

authored by

Dano Morrison and committed by
Teon L Brooks
5de658ee f7cd19df

+121 -58
+57 -15
app/epics/pyodideEpics.js
··· 1 1 import { combineEpics } from 'redux-observable'; 2 - import { of } from 'rxjs'; 2 + import { of, fromEvent } from 'rxjs'; 3 + import { toast } from 'react-toastify'; 3 4 import { map, mergeMap, tap, pluck, filter } from 'rxjs/operators'; 4 5 import { getWorkspaceDir } from '../utils/filesystem/storage'; 5 6 import { parseSingleQuoteJSON } from '../utils/pyodide/functions'; ··· 16 17 loadERP, 17 18 } from '../actions/pyodideActions'; 18 19 import { 19 - loadPackages, 20 - utils, 20 + loadPyodide, 21 21 loadCSV, 22 22 loadCleanedEpochs, 23 23 filterIIR, ··· 39 39 PYODIDE_STATUS, 40 40 } from '../constants/constants'; 41 41 42 - export const GET_EPOCHS_INFO = 'GET_EPOCHS_INFO'; 43 42 export const GET_CHANNEL_INFO = 'GET_CHANNEL_INFO'; 44 - export const SET_MAIN_CHANNEL = 'SET_MAIN_CHANNEL'; 45 - export const SET_EPOCH_INFO = 'SET_EPOCH_INFO'; 46 - export const SET_CHANNEL_INFO = 'SET_CHANNEL_INFO'; 47 - export const SET_PSD_PLOT = 'SET_PSD_PLOT'; 48 - export const SET_ERP_PLOT = 'SET_ERP_PLOT'; 49 - export const SET_TOPO_PLOT = 'SET_TOPO_PLOT'; 50 - export const SET_PYODIDE_STATUS = 'SET_PYODIDE_STATUS'; 43 + export const GET_EPOCHS_INFO = 'GET_EPOCHS_INFO'; 44 + export const RECEIVE_DISPLAY_DATA = 'RECEIVE_DISPLAY_DATA'; 45 + export const RECEIVE_ERROR = 'RECEIVE_ERROR'; 51 46 export const RECEIVE_EXECUTE_REPLY = 'RECEIVE_EXECUTE_REPLY'; 52 47 export const RECEIVE_EXECUTE_RESULT = 'RECEIVE_EXECUTE_RESULT'; 48 + export const RECEIVE_MESSAGE = 'RECEIVE_MESSAGE'; 53 49 export const RECEIVE_STREAM = 'RECEIVE_STREAM'; 54 - export const RECEIVE_DISPLAY_DATA = 'RECEIVE_DISPLAY_DATA'; 50 + export const SET_CHANNEL_INFO = 'SET_CHANNEL_INFO'; 51 + export const SET_EPOCH_INFO = 'SET_EPOCH_INFO'; 52 + export const SET_ERP_PLOT = 'SET_ERP_PLOT'; 53 + export const SET_MAIN_CHANNEL = 'SET_MAIN_CHANNEL'; 54 + export const SET_PSD_PLOT = 'SET_PSD_PLOT'; 55 + export const SET_PYODIDE_STATUS = 'SET_PYODIDE_STATUS'; 56 + export const SET_PYODIDE_WORKER = 'SET_PYODIDE_WORKER'; 57 + export const SET_TOPO_PLOT = 'SET_TOPO_PLOT'; 55 58 56 59 // ------------------------------------------------------------------------- 57 60 // Action Creators ··· 90 93 type: SET_PYODIDE_STATUS, 91 94 }); 92 95 96 + const setPyodideWorker = (payload: Worker) => ({ 97 + payload, 98 + type: SET_PYODIDE_WORKER, 99 + }); 100 + 101 + const receivePyodideError = (payload) => ({ 102 + payload, 103 + type: RECEIVE_ERROR, 104 + }); 105 + 106 + const receivePyodideMessage = (payload) => ({ 107 + payload, 108 + type: RECEIVE_MESSAGE, 109 + }); 110 + 93 111 // ------------------------------------------------------------------------- 94 112 // Epics 95 113 96 114 const launchEpic = (action$) => 97 115 action$.ofType(LAUNCH).pipe( 98 116 tap(() => console.log('launching')), 99 - mergeMap(loadPackages), 100 - mergeMap(utils), 101 - map(() => setPyodideStatus(PYODIDE_STATUS.LOADED)) 117 + map(loadPyodide), 118 + map(setPyodideWorker) 119 + ); 120 + 121 + const pyodideError = (action$) => 122 + action$.ofType(SET_PYODIDE_WORKER).pipe( 123 + pluck('payload'), 124 + tap((e) => 125 + toast.error(`Error in pyodideWorker at ${e.filename}, Line: ${e.lineno}, ${e.message}`) 126 + ), 127 + map(receivePyodideError) 128 + ); 129 + 130 + const pyodideMessage = (action$) => 131 + action$.ofType(SET_PYODIDE_WORKER).pipe( 132 + pluck('payload'), 133 + tap((e) => { 134 + const { results, error } = e.data; 135 + if (results && !error) { 136 + toast(`Pyodide: `, results); 137 + } else if (error) { 138 + toast.error('Pyodide: ', error); 139 + } 140 + }), 141 + map(receivePyodideMessage) 102 142 ); 103 143 104 144 const loadEpochsEpic = (action$, state$) => ··· 196 236 ); 197 237 198 238 export default combineEpics( 239 + pyodideError, 240 + pyodideMessage, 199 241 launchEpic, 200 242 loadEpochsEpic, 201 243 loadCleanedEpochsEpic,
+27 -42
app/utils/pyodide/index.js
··· 7 7 // This file contains the JS functions that allow the app to access python-wasm through pyodide 8 8 // These functions wrap the python strings defined in the 9 9 10 + 10 11 // ----------------------------- 11 12 // Imports and Utility functions 12 13 13 - // Note: this takes an incredibly long time 14 - export const loadPackages = async () => { 15 - await languagePluginLoader; 16 - console.log('loaded language plugin'); 17 - // using window.pyodide instead of pyodide to get linter to stop yelling ;) 18 - await window.pyodide.loadPackage(['matplotlib', 'mne', 'pandas']); 19 - await window.pyodide.runPython('import js'); 20 - console.log('loaded mne package'); 14 + export const loadPyodide = () => { 15 + return new Worker('./utils/pyodide/webworker.js'); 21 16 }; 22 17 23 18 export const loadUtils = async () => 24 - window.pyodide.runPython( 25 - readFileSync(path.join(__dirname, '/utils/pyodide/utils.py'), 'utf8') 26 - ); 19 + pyodideWorker.postMessage({ 20 + data: readFileSync(path.join(__dirname, '/utils/pyodide/utils.py'), 'utf8'), 21 + }); 27 22 28 23 export const loadCSV = async (csvArray: Array<any>) => { 29 24 window.csvArray = csvArray; 30 25 // TODO: Pass attached variable name as parameter to load_data 31 - await window.pyodide.runPython(`raw = load_data()`); 26 + await pyodideWorker.postMessage({ data: `raw = load_data()` }); 32 27 }; 33 28 34 29 // --------------------------- ··· 42 37 43 38 // NOTE: this command includes a ';' to prevent returning data 44 39 export const filterIIR = async (lowCutoff: number, highCutoff: number) => 45 - window.pyodide.runPython( 46 - `raw.filter(${lowCutoff}, ${highCutoff}, method='iir');` 47 - ); 40 + pyodideWorker.postMessage({ data: `raw.filter(${lowCutoff}, ${highCutoff}, method='iir');` }); 48 41 49 42 export const epochEvents = async ( 50 43 eventIDs: { [string]: number }, ··· 52 45 tmax: number, 53 46 reject?: Array<string> | string = 'None' 54 47 ) => 55 - window.pyodide.runPython( 56 - [ 48 + pyodideWorker.postMessage({ 49 + data: [ 57 50 `event_id = ${JSON.stringify(eventIDs)}`, 58 51 `tmin=${tmin}`, 59 52 `tmax=${tmax}`, ··· 64 57 `raw_epochs = Epochs(raw, events=events, event_id=event_id, 65 58 tmin=tmin, tmax=tmax, baseline=baseline, reject=reject, preload=True, 66 59 verbose=False, picks=picks)`, 67 - `conditions = OrderedDict({key: [value] for (key, value) in raw_epochs.event_id.items()})` 68 - ].join('\n') 69 - ); 60 + `conditions = OrderedDict({key: [value] for (key, value) in raw_epochs.event_id.items()})`, 61 + ].join('\n'), 62 + }); 70 63 71 64 export const requestEpochsInfo = async (variableName: string) => { 72 - const pyodideReturn = await window.pyodide.runPython( 73 - `get_epochs_info(${variableName})` 74 - ); 65 + const pyodideReturn = await pyodideWorker.postMessage({ 66 + data: `get_epochs_info(${variableName})`, 67 + }); 75 68 return pyodideReturn; 76 69 }; 77 70 78 71 export const requestChannelInfo = async () => 79 - window.pyodide.runPython( 80 - `[ch for ch in clean_epochs.ch_names if ch != 'Marker']` 81 - ); 72 + pyodideWorker.postMessage({ data: `[ch for ch in clean_epochs.ch_names if ch != 'Marker']` }); 82 73 83 74 // ----------------------------- 84 75 // Plot functions 85 76 86 77 export const cleanEpochsPlot = async () => { 87 78 // TODO: Figure out how to get image results from pyodide 88 - window.pyodide.runPython( 89 - `raw_epochs.plot(scalings='auto', n_epochs=6, title="Clean Data", events=None)` 90 - ); 79 + pyodideWorker.postMessage({ 80 + data: `raw_epochs.plot(scalings='auto', n_epochs=6, title="Clean Data", events=None)`, 81 + }); 91 82 }; 92 83 93 84 export const plotPSD = async () => { 94 85 // TODO: Figure out how to get image results from pyodide 95 - window.pyodide.runPython(`raw.plot_psd(fmin=1, fmax=30)`); 86 + pyodideWorker.postMessage({ data: `raw.plot_psd(fmin=1, fmax=30)` }); 96 87 }; 97 88 98 89 export const plotTopoMap = async () => { 99 90 // TODO: Figure out how to get image results from pyodide 100 - window.pyodide.runPython(`plot_topo(clean_epochs, conditions)`); 91 + pyodideWorker.postMessage({ data: `plot_topo(clean_epochs, conditions)` }); 101 92 }; 102 93 103 94 export const plotERP = (channelIndex: number) => ··· 105 96 ci=97.5, n_boot=1000, title='', diff_waveform=None)`; 106 97 107 98 export const saveEpochs = (workspaceDir: string, subject: string) => 108 - window.pyodide.runPython( 109 - `raw_epochs.save(${formatFilePath( 110 - path.join( 111 - workspaceDir, 112 - 'Data', 113 - subject, 114 - 'EEG', 115 - `${subject}-cleaned-epo.fif` 116 - ) 117 - )}` 118 - ); 99 + pyodideWorker.postMessage({ 100 + data: `raw_epochs.save(${formatFilePath( 101 + path.join(workspaceDir, 'Data', subject, 'EEG', `${subject}-cleaned-epo.fif`) 102 + )}`, 103 + });
-1
app/utils/pyodide/pyimport.py
··· 1 -
app/utils/pyodide/pythonStrings.js app/utils/pyodide/statements.json
+37
app/utils/pyodide/webworker.js
··· 1 + /** 2 + * This file has been copied from pyodide source and modified to allow 3 + * pyodide to be used in a web worker within this 4 + */ 5 + 6 + self.languagePluginUrl = './src'; 7 + importScripts('./pyodide.js'); 8 + 9 + const onmessage = function(e) { 10 + // eslint-disable-line no-unused-vars 11 + languagePluginLoader.then(() => { 12 + // Preloaded packages 13 + self.pyodide.loadPackage(['matplotlib', 'mne', 'pandas']).then(() => { 14 + const data = e.data; 15 + const keys = Object.keys(data); 16 + for (let key of keys) { 17 + if (key !== 'python') { 18 + // Keys other than python must be arguments for the python script. 19 + // Set them on self, so that `from js import key` works. 20 + self[key] = data[key]; 21 + } 22 + } 23 + 24 + self.pyodide 25 + .runPythonAsync(data.python, () => {}) 26 + .then((results) => { 27 + self.postMessage({ results }); 28 + }) 29 + .catch((err) => { 30 + // if you prefer messages with the error 31 + self.postMessage({ error: err.message }); 32 + // if you prefer onerror events 33 + // setTimeout(() => { throw err; }); 34 + }); 35 + }); 36 + }); 37 + };