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.

Added loading of pyodide within app epics

authored by

jdpigeon and committed by
Teon L Brooks
99c93850 4e58b5c6

+123 -65
+1 -8
app/constants/constants.ts
··· 37 37 BLUETOOTH_DISABLED = 'BLUETOOTH_DISABLED', 38 38 } 39 39 40 - export enum KERNEL_STATUS { 41 - OFFLINE = 'Offline', 42 - BUSY = 'Busy', 43 - IDLE = 'Idle', 44 - STARTING = 'Starting', 45 - } 46 - 47 40 export enum DEVICE_AVAILABILITY { 48 41 NONE = 'NONE', 49 42 SEARCHING = 'SEARCHING', 50 43 AVAILABLE = 'AVAILABLE', 51 44 } 52 45 53 - // Names of variables in the pyodide kernel 46 + // Names of variables in pyodide 54 47 export enum PYODIDE_VARIABLE_NAMES { 55 48 RAW_EPOCHS = 'raw_epochs', 56 49 CLEAN_EPOCHS = 'clean_epochs',
+20
app/containers/HomeContainer.js
··· 1 + // @flow 2 + import { connect } from "react-redux"; 3 + import { bindActionCreators } from "redux"; 4 + import Home from "../components/HomeComponent"; 5 + import * as deviceActions from "../actions/deviceActions"; 6 + import * as pyodideActions from "../actions/pyodideActions"; 7 + import * as experimentActions from "../actions/experimentActions"; 8 + 9 + function mapDispatchToProps(dispatch) { 10 + return { 11 + deviceActions: bindActionCreators(deviceActions, dispatch), 12 + pyodideActions: bindActionCreators(pyodideActions, dispatch), 13 + experimentActions: bindActionCreators(experimentActions, dispatch) 14 + }; 15 + } 16 + 17 + export default connect( 18 + null, 19 + mapDispatchToProps 20 + )(Home);
+56 -41
app/epics/pyodideEpics.js
··· 1 - import { combineEpics } from 'redux-observable'; 2 - import { from, of } from 'rxjs'; 1 + import { combineEpics } from "redux-observable"; 2 + import { of } from "rxjs"; 3 3 import { 4 4 map, 5 5 mergeMap, 6 6 tap, 7 7 pluck, 8 8 ignoreElements, 9 - filter, 10 - take 11 - } from 'rxjs/operators'; 12 - import { isNil } from 'lodash'; 13 - import { toast } from 'react-toastify'; 14 - import { getWorkspaceDir } from '../utils/filesystem/storage'; 15 - import { languagePluginLoader } from '../../utils/pyodide/pyodide'; 9 + filter 10 + } from "rxjs/operators"; 11 + import { getWorkspaceDir } from "../utils/filesystem/storage"; 12 + import { languagePluginLoader } from "../utils/pyodide/pyodide"; 16 13 import { 17 14 LAUNCH, 18 15 LOAD_EPOCHS, ··· 23 20 CLEAN_EPOCHS, 24 21 loadTopo, 25 22 loadERP 26 - } from '../actions/pyodideActions'; 23 + } from "../actions/pyodideActions"; 27 24 import { 25 + test, 28 26 imports, 29 - utils, 30 27 loadCSV, 31 28 loadCleanedEpochs, 32 29 filterIIR, ··· 38 35 plotERP, 39 36 plotTopoMap, 40 37 saveEpochs 41 - } from '../utils/pyodide/commands'; 38 + } from "../utils/pyodide/commands"; 42 39 import { 43 40 EMOTIV_CHANNELS, 44 41 EVENTS, 45 42 DEVICES, 46 43 MUSE_CHANNELS, 47 - PYODIDE_VARIABLE_NAMES 48 - } from '../constants/constants'; 44 + PYODIDE_VARIABLE_NAMES, 45 + PYODIDE_STATUS 46 + } from "../constants/constants"; 49 47 50 - export const GET_EPOCHS_INFO = 'GET_EPOCHS_INFO'; 51 - export const GET_CHANNEL_INFO = 'GET_CHANNEL_INFO'; 52 - export const SET_MAIN_CHANNEL = 'SET_MAIN_CHANNEL'; 53 - export const SET_EPOCH_INFO = 'SET_EPOCH_INFO'; 54 - export const SET_CHANNEL_INFO = 'SET_CHANNEL_INFO'; 55 - export const SET_PSD_PLOT = 'SET_PSD_PLOT'; 56 - export const SET_ERP_PLOT = 'SET_ERP_PLOT'; 57 - export const SET_TOPO_PLOT = 'SET_TOPO_PLOT'; 58 - export const RECEIVE_EXECUTE_REPLY = 'RECEIVE_EXECUTE_REPLY'; 59 - export const RECEIVE_EXECUTE_RESULT = 'RECEIVE_EXECUTE_RESULT'; 60 - export const RECEIVE_STREAM = 'RECEIVE_STREAM'; 61 - export const RECEIVE_DISPLAY_DATA = 'RECEIVE_DISPLAY_DATA'; 48 + export const GET_EPOCHS_INFO = "GET_EPOCHS_INFO"; 49 + export const GET_CHANNEL_INFO = "GET_CHANNEL_INFO"; 50 + export const SET_MAIN_CHANNEL = "SET_MAIN_CHANNEL"; 51 + export const SET_EPOCH_INFO = "SET_EPOCH_INFO"; 52 + export const SET_CHANNEL_INFO = "SET_CHANNEL_INFO"; 53 + export const SET_PSD_PLOT = "SET_PSD_PLOT"; 54 + export const SET_ERP_PLOT = "SET_ERP_PLOT"; 55 + export const SET_TOPO_PLOT = "SET_TOPO_PLOT"; 56 + export const SET_PYODIDE_STATUS = "SET_PYODIDE_STATUS"; 57 + export const RECEIVE_EXECUTE_REPLY = "RECEIVE_EXECUTE_REPLY"; 58 + export const RECEIVE_EXECUTE_RESULT = "RECEIVE_EXECUTE_RESULT"; 59 + export const RECEIVE_STREAM = "RECEIVE_STREAM"; 60 + export const RECEIVE_DISPLAY_DATA = "RECEIVE_DISPLAY_DATA"; 62 61 63 62 // ------------------------------------------------------------------------- 64 63 // Action Creators ··· 67 66 68 67 const getChannelInfo = () => ({ type: GET_CHANNEL_INFO }); 69 68 70 - const setMainChannel = payload => ({ 71 - payload, 72 - type: SET_MAIN_CHANNEL, 73 - }); 74 - 75 - const setEpochInfo = (payload) => ({ 69 + const setEpochInfo = payload => ({ 76 70 payload, 77 71 type: SET_EPOCH_INFO, 78 72 }); ··· 97 91 type: SET_ERP_PLOT, 98 92 }); 99 93 100 - const receiveExecuteReply = (payload) => ({ 94 + const setPyodideStatus = payload => ({ 95 + payload, 96 + type: SET_PYODIDE_STATUS 97 + }); 98 + 99 + const receiveExecuteReply = payload => ({ 101 100 payload, 102 101 type: RECEIVE_EXECUTE_REPLY, 103 102 }); ··· 120 119 // ------------------------------------------------------------------------- 121 120 // Epics 122 121 123 - const launchEpic = (action$, state$) => 122 + const launchEpic = action$ => 124 123 action$.ofType(LAUNCH).pipe( 125 - mergeMap(languagePluginLoader), 126 - tap(() => console.log('launched pyodide')) 124 + tap(() => console.log("launching")), 125 + mergeMap(async () => { 126 + await languagePluginLoader; 127 + console.log("loaded language plugin"); 128 + // using window.pyodide instead of pyodide to get linter to stop yelling ;) 129 + await window.pyodide.loadPackage(["mne"]); 130 + console.log("loaded mne package"); 131 + }), 132 + map(() => setPyodideStatus(PYODIDE_STATUS.LOADED)) 127 133 ); 128 134 129 135 const loadEpochsEpic = (action$, state$) => 130 136 action$.ofType(LOAD_EPOCHS).pipe( 131 - pluck('payload'), 137 + pluck("payload"), 132 138 filter(filePathsArray => filePathsArray.length >= 1), 133 139 map(filePathsArray => loadCSV(filePathsArray)), 134 140 map(() => filterIIR(1, 30)), ··· 150 156 151 157 const loadCleanedEpochsEpic = (action$, state$) => 152 158 action$.ofType(LOAD_CLEANED_EPOCHS).pipe( 153 - pluck('payload'), 159 + pluck("payload"), 154 160 filter(filePathsArray => filePathsArray.length >= 1), 155 161 map(filePathsArray => loadCleanedEpochs(filePathsArray)), 156 162 mergeMap(() => ··· 176 182 177 183 const getEpochsInfoEpic = (action$, state$) => 178 184 action$.ofType(GET_EPOCHS_INFO).pipe( 179 - pluck('payload'), 185 + pluck("payload"), 180 186 map(variableName => requestEpochsInfo(variableName)), 181 187 map(epochInfoString => 182 188 parseSingleQuoteJSON(epochInfoString).map(infoObj => ({ ··· 216 222 217 223 const loadERPEpic = (action$, state$) => 218 224 action$.ofType(LOAD_ERP).pipe( 225 + <<<<<<< HEAD 219 226 pluck('payload'), 220 227 map((channelName) => { 228 + ======= 229 + pluck("payload"), 230 + map(channelName => { 231 + >>>>>>> Added loading of pyodide within app epics 221 232 if (MUSE_CHANNELS.includes(channelName)) { 222 233 return MUSE_CHANNELS.indexOf(channelName); 223 234 } else if (EMOTIV_CHANNELS.includes(channelName)) { 224 235 return EMOTIV_CHANNELS.indexOf(channelName); 225 236 } 237 + <<<<<<< HEAD 226 238 console.warn('channel name supplied to loadERPEpic does not belong to either device'); 239 + ======= 240 + console.warn( 241 + "channel name supplied to loadERPEpic does not belong to either device" 242 + ); 243 + >>>>>>> Added loading of pyodide within app epics 227 244 return EMOTIV_CHANNELS[0]; 228 245 }), 229 246 map(channelIndex => plotERP(channelIndex)), ··· 232 249 233 250 export default combineEpics( 234 251 launchEpic, 235 - setUpChannelEpic, 236 - receiveChannelMessageEpic, 237 252 loadEpochsEpic, 238 253 loadCleanedEpochsEpic, 239 254 cleanEpochsEpic,
+13 -4
app/reducers/pyodideReducer.js
··· 6 6 SET_PSD_PLOT, 7 7 SET_TOPO_PLOT, 8 8 SET_ERP_PLOT, 9 - RECEIVE_EXECUTE_RETURN 10 - } from '../epics/pyodideEpics'; 11 - import { ActionType } from '../constants/interfaces'; 12 - import { EXPERIMENT_CLEANUP } from '../epics/experimentEpics'; 9 + RECEIVE_EXECUTE_RETURN, 10 + SET_PYODIDE_STATUS 11 + } from "../epics/pyodideEpics"; 12 + import { ActionType } from "../constants/interfaces"; 13 + import { PYODIDE_STATUS } from "../constants/constants"; 14 + import { EXPERIMENT_CLEANUP } from "../epics/experimentEpics"; 13 15 14 16 export interface PyodideStateType { 15 17 +mainChannel: ?any; ··· 27 29 psdPlot: null, 28 30 topoPlot: null, 29 31 erpPlot: null, 32 + status: PYODIDE_STATUS.NOT_LOADED 30 33 }; 31 34 32 35 export default function pyodide( ··· 80 83 81 84 case RECEIVE_EXECUTE_RETURN: 82 85 return state; 86 + 87 + case SET_PYODIDE_STATUS: 88 + return { 89 + ...state, 90 + status: action.payload 91 + }; 83 92 84 93 default: 85 94 return state;
+33 -12
app/utils/pyodide/commands.py app/utils/pyodide/commands.js
··· 1 - import * as path from 'path'; 2 - import { readFileSync } from 'fs'; 1 + import * as path from "path"; 2 + import { readFileSync } from "fs"; 3 3 4 + let pyodide; 4 5 // ----------------------------- 5 6 // Imports and Utility functions 6 7 8 + export const test = async () => { 9 + await window.pyodide.loadPackage(["mne"]); 10 + 11 + const mneCommands = [ 12 + `import numpy as np`, 13 + `import mne`, 14 + `data = np.repeat(np.atleast_2d(np.arange(1000)), 8, axis=0)`, 15 + `info = mne.create_info(8, 250)`, 16 + `raw = mne.io.RawArray(data=data, info=info)`, 17 + `raw.save("test_brainwaves.fif")` 18 + ]; 19 + await window.pyodide.runPython(mneCommands.join("; ")); 20 + }; 21 + 22 + export const loadPackages = async () => window.pyodide.loadPackage(["mne"]); 23 + 7 24 export const imports = () => 8 - readFileSync(path.join(__dirname, '/utils/pyodide/pyimport.py'), 'utf8'); 25 + pyodide.runPython( 26 + readFileSync(path.join(__dirname, "/utils/pyodide/pyimport.py"), "utf8") 27 + ); 9 28 10 29 export const utils = () => 11 - readFileSync(path.join(__dirname, '/utils/pyodide/utils.py'), 'utf8'); 30 + pyodide.runPython( 31 + readFileSync(path.join(__dirname, "/utils/pyodide/utils.py"), "utf8") 32 + ); 12 33 13 34 export const loadCSV = (filePathArray: Array<string>) => 14 35 [ 15 36 `files = [${filePathArray.map(filePath => formatFilePath(filePath))}]`, 16 37 `replace_ch_names = None`, 17 38 `raw = load_data(files, replace_ch_names)` 18 - ].join('\n'); 39 + ].join("\n"); 19 40 20 41 // --------------------------- 21 42 // MNE-Related Data Processing ··· 24 45 `files = [${filePathArray.map(filePath => formatFilePath(filePath))}]`, 25 46 `clean_epochs = concatenate_epochs([read_epochs(file) for file in files])`, 26 47 `conditions = OrderedDict({key: [value] for (key, value) in clean_epochs.event_id.items()})` 27 - ].join('\n'); 48 + ].join("\n"); 28 49 29 50 // NOTE: this command includes a ';' to prevent returning data 30 51 export const filterIIR = (lowCutoff: number, highCutoff: number) => ··· 34 55 eventIDs: { [string]: number }, 35 56 tmin: number, 36 57 tmax: number, 37 - reject?: Array<string> | string = 'None' 58 + reject?: Array<string> | string = "None" 38 59 ) => { 39 60 const command = [ 40 61 `event_id = ${JSON.stringify(eventIDs)}`, ··· 43 64 `baseline= (tmin, tmax)`, 44 65 `picks = None`, 45 66 `reject = ${reject}`, 46 - 'events = find_events(raw)', 67 + "events = find_events(raw)", 47 68 `raw_epochs = Epochs(raw, events=events, event_id=event_id, 48 69 tmin=tmin, tmax=tmax, baseline=baseline, reject=reject, preload=True, 49 70 verbose=False, picks=picks)`, 50 71 `conditions = OrderedDict({key: [value] for (key, value) in raw_epochs.event_id.items()})` 51 - ].join('\n'); 72 + ].join("\n"); 52 73 return command; 53 74 }; 54 75 ··· 76 97 `raw_epochs.save(${formatFilePath( 77 98 path.join( 78 99 workspaceDir, 79 - 'Data', 100 + "Data", 80 101 subject, 81 - 'EEG', 102 + "EEG", 82 103 `${subject}-cleaned-epo.fif` 83 104 ) 84 105 )})`; ··· 87 108 // Helper methods 88 109 89 110 const formatFilePath = (filePath: string) => 90 - `"${filePath.replace(/\\/g, '/')}"`; 111 + `"${filePath.replace(/\\/g, "/")}"`;