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.

rename files from jupyter to pyodide

to make it clear that python is being served using WebAssembly and not the jupyter, changing all the references

+571
+52
app/actions/pyodideActions.js
··· 1 + // ------------------------------------------------------------------------- 2 + // Action Types 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'; 14 + 15 + // ------------------------------------------------------------------------- 16 + // Actions 17 + 18 + export const launchKernel = () => ({ type: LAUNCH_KERNEL }); 19 + 20 + export const requestKernelInfo = () => ({ type: REQUEST_KERNEL_INFO }); 21 + 22 + export const sendExecuteRequest = (payload: string) => ({ 23 + payload, 24 + type: SEND_EXECUTE_REQUEST, 25 + }); 26 + 27 + export const loadEpochs = (payload: Array<string>) => ({ 28 + payload, 29 + type: LOAD_EPOCHS, 30 + }); 31 + 32 + export const loadCleanedEpochs = (payload: Array<string>) => ({ 33 + payload, 34 + type: LOAD_CLEANED_EPOCHS, 35 + }); 36 + 37 + export const loadPSD = () => ({ 38 + type: LOAD_PSD, 39 + }); 40 + 41 + export const loadERP = (payload: ?string) => ({ 42 + payload, 43 + type: LOAD_ERP, 44 + }); 45 + 46 + export const loadTopo = () => ({ 47 + type: LOAD_TOPO, 48 + }); 49 + 50 + export const cleanEpochs = () => ({ type: CLEAN_EPOCHS }); 51 + 52 + export const closeKernel = () => ({ type: CLOSE_KERNEL });
+412
app/epics/pyodideEpics.js
··· 1 + import { combineEpics } from 'redux-observable'; 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'; 7 + import { isNil } from 'lodash'; 8 + import { kernelInfoRequest, executeRequest } from '@nteract/messaging'; 9 + import { toast } from 'react-toastify'; 10 + import { getWorkspaceDir } from '../utils/filesystem/storage'; 11 + import { 12 + LAUNCH_KERNEL, 13 + REQUEST_KERNEL_INFO, 14 + LOAD_EPOCHS, 15 + LOAD_CLEANED_EPOCHS, 16 + LOAD_PSD, 17 + LOAD_ERP, 18 + LOAD_TOPO, 19 + CLEAN_EPOCHS, 20 + CLOSE_KERNEL, 21 + loadTopo, 22 + loadERP 23 + } from '../actions/pyodideActions'; 24 + import { 25 + imports, 26 + utils, 27 + loadCSV, 28 + loadCleanedEpochs, 29 + filterIIR, 30 + epochEvents, 31 + requestEpochsInfo, 32 + requestChannelInfo, 33 + cleanEpochsPlot, 34 + plotPSD, 35 + plotERP, 36 + plotTopoMap, 37 + saveEpochs 38 + } from '../utils/pyodide/commands'; 39 + import { 40 + EMOTIV_CHANNELS, 41 + EVENTS, 42 + DEVICES, 43 + MUSE_CHANNELS, 44 + JUPYTER_VARIABLE_NAMES, 45 + } from '../constants/constants'; 46 + import { 47 + parseSingleQuoteJSON, 48 + parseKernelStatus, 49 + debugParseMessage 50 + } from '../utils/pyodide/functions'; 51 + 52 + export const GET_EPOCHS_INFO = 'GET_EPOCHS_INFO'; 53 + export const GET_CHANNEL_INFO = 'GET_CHANNEL_INFO'; 54 + export const SET_KERNEL = 'SET_KERNEL'; 55 + export const SET_KERNEL_STATUS = 'SET_KERNEL_STATUS'; 56 + export const SET_KERNEL_INFO = 'SET_KERNEL_INFO'; 57 + export const SET_MAIN_CHANNEL = 'SET_MAIN_CHANNEL'; 58 + export const SET_EPOCH_INFO = 'SET_EPOCH_INFO'; 59 + export const SET_CHANNEL_INFO = 'SET_CHANNEL_INFO'; 60 + export const SET_PSD_PLOT = 'SET_PSD_PLOT'; 61 + export const SET_ERP_PLOT = 'SET_ERP_PLOT'; 62 + export const SET_TOPO_PLOT = 'SET_TOPO_PLOT'; 63 + export const RECEIVE_EXECUTE_REPLY = 'RECEIVE_EXECUTE_REPLY'; 64 + export const RECEIVE_EXECUTE_RESULT = 'RECEIVE_EXECUTE_RESULT'; 65 + export const RECEIVE_STREAM = 'RECEIVE_STREAM'; 66 + export const RECEIVE_DISPLAY_DATA = 'RECEIVE_DISPLAY_DATA'; 67 + 68 + // ------------------------------------------------------------------------- 69 + // Action Creators 70 + 71 + const getEpochsInfo = (payload) => ({ payload, type: GET_EPOCHS_INFO }); 72 + 73 + const getChannelInfo = () => ({ type: GET_CHANNEL_INFO }); 74 + 75 + const setKernel = (payload) => ({ 76 + payload, 77 + type: SET_KERNEL, 78 + }); 79 + 80 + const setKernelStatus = (payload) => ({ 81 + payload, 82 + type: SET_KERNEL_STATUS, 83 + }); 84 + 85 + const setKernelInfo = (payload) => ({ 86 + payload, 87 + type: SET_KERNEL_INFO, 88 + }); 89 + 90 + const setMainChannel = (payload) => ({ 91 + payload, 92 + type: SET_MAIN_CHANNEL, 93 + }); 94 + 95 + const setEpochInfo = (payload) => ({ 96 + payload, 97 + type: SET_EPOCH_INFO, 98 + }); 99 + 100 + const setChannelInfo = (payload) => ({ 101 + payload, 102 + type: SET_CHANNEL_INFO, 103 + }); 104 + 105 + const setPSDPlot = (payload) => ({ 106 + payload, 107 + type: SET_PSD_PLOT, 108 + }); 109 + 110 + const setTopoPlot = (payload) => ({ 111 + payload, 112 + type: SET_TOPO_PLOT, 113 + }); 114 + 115 + const setERPPlot = (payload) => ({ 116 + payload, 117 + type: SET_ERP_PLOT, 118 + }); 119 + 120 + const receiveExecuteReply = (payload) => ({ 121 + payload, 122 + type: RECEIVE_EXECUTE_REPLY, 123 + }); 124 + 125 + const receiveExecuteResult = (payload) => ({ 126 + payload, 127 + type: RECEIVE_EXECUTE_RESULT, 128 + }); 129 + 130 + const receiveDisplayData = (payload) => ({ 131 + payload, 132 + type: RECEIVE_DISPLAY_DATA, 133 + }); 134 + 135 + const receiveStream = (payload) => ({ 136 + payload, 137 + type: RECEIVE_STREAM, 138 + }); 139 + 140 + // ------------------------------------------------------------------------- 141 + // Epics 142 + 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 + const loadEpochsEpic = (action$, state$) => 223 + action$.ofType(LOAD_EPOCHS).pipe( 224 + pluck('payload'), 225 + filter((filePathsArray) => filePathsArray.length >= 1), 226 + map((filePathsArray) => 227 + state$.value.jupyter.mainChannel.next(executeRequest(loadCSV(filePathsArray))) 228 + ), 229 + awaitOkMessage(action$), 230 + execute(filterIIR(1, 30), state$), 231 + awaitOkMessage(action$), 232 + map(() => 233 + epochEvents( 234 + { 235 + [state$.value.experiment.params.stimulus1.title]: EVENTS.STIMULUS_1, 236 + [state$.value.experiment.params.stimulus2.title]: EVENTS.STIMULUS_2, 237 + [state$.value.experiment.params.stimulus3.title]: EVENTS.STIMULUS_3, 238 + [state$.value.experiment.params.stimulus4.title]: EVENTS.STIMULUS_4, 239 + }, 240 + -0.1, 241 + 0.8 242 + ) 243 + ), 244 + tap((e)=> {console.log('e', e)}), 245 + map((epochEventsCommand) => 246 + state$.value.jupyter.mainChannel.next(executeRequest(epochEventsCommand)) 247 + ), 248 + awaitOkMessage(action$), 249 + map(() => getEpochsInfo(JUPYTER_VARIABLE_NAMES.RAW_EPOCHS)) 250 + ); 251 + 252 + const loadCleanedEpochsEpic = (action$, state$) => 253 + action$.ofType(LOAD_CLEANED_EPOCHS).pipe( 254 + pluck('payload'), 255 + filter((filePathsArray) => filePathsArray.length >= 1), 256 + map((filePathsArray) => 257 + state$.value.jupyter.mainChannel.next(executeRequest(loadCleanedEpochs(filePathsArray))) 258 + ), 259 + awaitOkMessage(action$), 260 + mergeMap(() => 261 + of(getEpochsInfo(JUPYTER_VARIABLE_NAMES.CLEAN_EPOCHS), getChannelInfo(), loadTopo()) 262 + ) 263 + ); 264 + 265 + const cleanEpochsEpic = (action$, state$) => 266 + action$.ofType(CLEAN_EPOCHS).pipe( 267 + execute(cleanEpochsPlot(), state$), 268 + mergeMap(() => 269 + action$.ofType(RECEIVE_STREAM).pipe( 270 + pluck('payload'), 271 + filter( 272 + (msg) => msg.channel === 'iopub' && msg.content.text.includes('Channels marked as bad') 273 + ), 274 + take(1) 275 + ) 276 + ), 277 + map(() => 278 + state$.value.jupyter.mainChannel.next( 279 + executeRequest( 280 + saveEpochs( 281 + getWorkspaceDir(state$.value.experiment.title), 282 + state$.value.experiment.subject 283 + ) 284 + ) 285 + ) 286 + ), 287 + awaitOkMessage(action$), 288 + map(() => getEpochsInfo(JUPYTER_VARIABLE_NAMES.RAW_EPOCHS)) 289 + ); 290 + 291 + const getEpochsInfoEpic = (action$, state$) => 292 + action$.ofType(GET_EPOCHS_INFO).pipe( 293 + pluck('payload'), 294 + map((variableName) => 295 + state$.value.jupyter.mainChannel.next(executeRequest(requestEpochsInfo(variableName))) 296 + ), 297 + mergeMap(() => 298 + action$.ofType(RECEIVE_EXECUTE_RESULT).pipe( 299 + pluck('payload'), 300 + filter((msg) => msg.channel === 'iopub' && !isNil(msg.content.data)), 301 + pluck('content', 'data', 'text/plain'), 302 + filter((msg) => msg.includes('Drop Percentage')), 303 + take(1) 304 + ) 305 + ), 306 + map((epochInfoString) => 307 + parseSingleQuoteJSON(epochInfoString).map((infoObj) => ({ 308 + name: Object.keys(infoObj)[0], 309 + value: infoObj[Object.keys(infoObj)[0]], 310 + })) 311 + ), 312 + map(setEpochInfo) 313 + ); 314 + 315 + const getChannelInfoEpic = (action$, state$) => 316 + action$.ofType(GET_CHANNEL_INFO).pipe( 317 + execute(requestChannelInfo(), state$), 318 + mergeMap(() => 319 + action$.ofType(RECEIVE_EXECUTE_RESULT).pipe( 320 + pluck('payload'), 321 + filter((msg) => msg.channel === 'iopub' && !isNil(msg.content.data)), 322 + pluck('content', 'data', 'text/plain'), 323 + // Filter to prevent this from reading requestEpochsInfo returns 324 + filter((msg) => !msg.includes('Drop Percentage')), 325 + take(1) 326 + ) 327 + ), 328 + map((channelInfoString) => setChannelInfo(parseSingleQuoteJSON(channelInfoString))) 329 + ); 330 + 331 + const loadPSDEpic = (action$, state$) => 332 + action$.ofType(LOAD_PSD).pipe( 333 + execute(plotPSD(), state$), 334 + mergeMap(() => 335 + action$.ofType(RECEIVE_DISPLAY_DATA).pipe( 336 + pluck('payload'), 337 + // PSD graphs should have two axes 338 + filter((msg) => msg.content.data['text/plain'].includes('2 Axes')), 339 + pluck('content', 'data'), 340 + take(1) 341 + ) 342 + ), 343 + map(setPSDPlot) 344 + ); 345 + 346 + const loadTopoEpic = (action$, state$) => 347 + action$.ofType(LOAD_TOPO).pipe( 348 + execute(plotTopoMap(), state$), 349 + mergeMap(() => 350 + action$.ofType(RECEIVE_DISPLAY_DATA).pipe(pluck('payload'), pluck('content', 'data'), take(1)) 351 + ), 352 + mergeMap((topoPlot) => 353 + of( 354 + setTopoPlot(topoPlot), 355 + loadERP( 356 + state$.value.device.deviceType === DEVICES.EMOTIV ? EMOTIV_CHANNELS[0] : MUSE_CHANNELS[0] 357 + ) 358 + ) 359 + ) 360 + ); 361 + 362 + const loadERPEpic = (action$, state$) => 363 + action$.ofType(LOAD_ERP).pipe( 364 + pluck('payload'), 365 + map((channelName) => { 366 + if (MUSE_CHANNELS.includes(channelName)) { 367 + return MUSE_CHANNELS.indexOf(channelName); 368 + } else if (EMOTIV_CHANNELS.includes(channelName)) { 369 + return EMOTIV_CHANNELS.indexOf(channelName); 370 + } 371 + console.warn('channel name supplied to loadERPEpic does not belong to either device'); 372 + return EMOTIV_CHANNELS[0]; 373 + }), 374 + map((channelIndex) => 375 + state$.value.jupyter.mainChannel.next(executeRequest(plotERP(channelIndex))) 376 + ), 377 + mergeMap(() => 378 + action$.ofType(RECEIVE_DISPLAY_DATA).pipe( 379 + pluck('payload'), 380 + // ERP graphs should have 1 axis according to MNE 381 + filter((msg) => msg.content.data['text/plain'].includes('1 Axes')), 382 + pluck('content', 'data'), 383 + take(1) 384 + ) 385 + ), 386 + map(setERPPlot) 387 + ); 388 + 389 + const closeKernelEpic = (action$, state$) => 390 + action$.ofType(CLOSE_KERNEL).pipe( 391 + map(() => { 392 + state$.value.jupyter.kernel.spawn.kill(); 393 + state$.value.jupyter.mainChannel.complete(); 394 + }), 395 + ignoreElements() 396 + ); 397 + 398 + export default combineEpics( 399 + launchEpic, 400 + setUpChannelEpic, 401 + requestKernelInfoEpic, 402 + receiveChannelMessageEpic, 403 + loadEpochsEpic, 404 + loadCleanedEpochsEpic, 405 + cleanEpochsEpic, 406 + getEpochsInfoEpic, 407 + getChannelInfoEpic, 408 + loadPSDEpic, 409 + loadTopoEpic, 410 + loadERPEpic, 411 + closeKernelEpic 412 + );
+107
app/reducers/pyodideReducer.js
··· 1 + // @flow 2 + import { 3 + SET_KERNEL, 4 + SET_KERNEL_STATUS, 5 + SET_MAIN_CHANNEL, 6 + SET_KERNEL_INFO, 7 + SET_EPOCH_INFO, 8 + SET_CHANNEL_INFO, 9 + SET_PSD_PLOT, 10 + SET_TOPO_PLOT, 11 + SET_ERP_PLOT, 12 + RECEIVE_EXECUTE_RETURN 13 + } from '../epics/pyodideEpics'; 14 + import { ActionType, Kernel } from '../constants/interfaces'; 15 + import { KERNEL_STATUS } from '../constants/constants'; 16 + import { EXPERIMENT_CLEANUP } from '../epics/experimentEpics'; 17 + 18 + export interface JupyterStateType { 19 + +kernel: ?Kernel; 20 + +kernelStatus: KERNEL_STATUS; 21 + +mainChannel: ?any; 22 + +epochsInfo: ?Array<{ [string]: number | string }>; 23 + +channelInfo: ?Array<string>; 24 + +psdPlot: ?{ [string]: string }; 25 + +topoPlot: ?{ [string]: string }; 26 + +erpPlot: ?{ [string]: string }; 27 + } 28 + 29 + const initialState = { 30 + kernel: null, 31 + kernelStatus: KERNEL_STATUS.OFFLINE, 32 + mainChannel: null, 33 + epochsInfo: null, 34 + channelInfo: [], 35 + psdPlot: null, 36 + topoPlot: null, 37 + erpPlot: null, 38 + }; 39 + 40 + export default function jupyter(state: JupyterStateType = initialState, action: ActionType) { 41 + switch (action.type) { 42 + case SET_KERNEL: 43 + return { 44 + ...state, 45 + kernel: action.payload, 46 + }; 47 + 48 + case SET_KERNEL_STATUS: 49 + return { 50 + ...state, 51 + kernelStatus: action.payload, 52 + }; 53 + 54 + case SET_MAIN_CHANNEL: 55 + return { 56 + ...state, 57 + mainChannel: action.payload, 58 + }; 59 + 60 + case SET_KERNEL_INFO: 61 + return state; 62 + 63 + case SET_EPOCH_INFO: 64 + return { 65 + ...state, 66 + epochsInfo: action.payload, 67 + }; 68 + 69 + case SET_CHANNEL_INFO: 70 + return { 71 + ...state, 72 + channelInfo: action.payload, 73 + }; 74 + 75 + case SET_PSD_PLOT: 76 + return { 77 + ...state, 78 + psdPlot: action.payload, 79 + }; 80 + 81 + case SET_TOPO_PLOT: 82 + return { 83 + ...state, 84 + topoPlot: action.payload, 85 + }; 86 + 87 + case SET_ERP_PLOT: 88 + return { 89 + ...state, 90 + erpPlot: action.payload, 91 + }; 92 + 93 + case EXPERIMENT_CLEANUP: 94 + return { 95 + ...state, 96 + epochsInfo: null, 97 + psdPlot: null, 98 + erpPlot: null, 99 + }; 100 + 101 + case RECEIVE_EXECUTE_RETURN: 102 + return state; 103 + 104 + default: 105 + return state; 106 + } 107 + }