diff --git a/app/components/HomeComponent/index.tsx b/app/components/HomeComponent/index.tsx index 886d15b..a7a3c53 100644 --- a/app/components/HomeComponent/index.tsx +++ b/app/components/HomeComponent/index.tsx @@ -39,6 +39,7 @@ import OverviewComponent from './OverviewComponent'; import EEGExplorationComponent from '../EEGExplorationComponent'; import { SignalQualityData } from '../../constants/interfaces'; import { getExperimentFromType } from '../../utils/labjs/functions'; +import PyodidePlotWidget from '../PyodidePlotWidget'; const { dialog } = remote; @@ -47,6 +48,7 @@ const HOME_STEPS = { RECENT: 'MY EXPERIMENTS', NEW: 'EXPERIMENT BANK', EXPLORE: 'EXPLORE EEG DATA', + PYODIDE_TEST: 'PYODIDE_TEST', }; export interface Props { @@ -61,6 +63,9 @@ export interface Props { history: History; PyodideActions: typeof PyodideActions; signalQualityObservable?: Observable; + topoPlot: { + [key: string]: string; + }; } interface State { @@ -360,6 +365,25 @@ export default class Home extends Component { DeviceActions={this.props.DeviceActions} /> ); + case HOME_STEPS.PYODIDE_TEST: + return ( + + + + + + + + + + + ); } } diff --git a/app/containers/HomeContainer.ts b/app/containers/HomeContainer.ts index bc43932..191db9c 100644 --- a/app/containers/HomeContainer.ts +++ b/app/containers/HomeContainer.ts @@ -6,6 +6,7 @@ import { DeviceActions, ExperimentActions, PyodideActions } from '../actions'; function mapStateToProps(state) { return { ...state.device, + ...state.pyodide, }; } diff --git a/app/epics/pyodideEpics.ts b/app/epics/pyodideEpics.ts index be17074..bb8d94b 100644 --- a/app/epics/pyodideEpics.ts +++ b/app/epics/pyodideEpics.ts @@ -17,8 +17,10 @@ import { plotPSD, plotERP, plotTopoMap, + plotTestPlot, saveEpochs, loadPyodide, + loadUtils, } from '../utils/pyodide'; import { EMOTIV_CHANNELS, @@ -39,7 +41,11 @@ const launchEpic: Epic = ( action$.pipe( filter(isActionOf(PyodideActions.Launch)), tap(() => console.log('launching')), - map(loadPyodide), + mergeMap(loadPyodide), + tap((worker) => { + console.log('loadPyodide completed, laoding utils'); + // loadUtils(worker); + }), map(PyodideActions.SetPyodideWorker) ); @@ -81,9 +87,9 @@ const pyodideMessageEpic: Epic< const { results, error } = e.data; if (results && !error) { - toast(`Pyodide: `, results); + toast.error(`Pyodide: ${results}`); } else if (error) { - toast.error('Pyodide: ', error); + toast.error(`Pyodide: ${error}`); } }), map(PyodideActions.ReceiveMessage) @@ -98,14 +104,15 @@ const loadEpochsEpic: Epic = ( pluck('payload'), filter((filePathsArray: string[]) => filePathsArray.length >= 1), map((filePathsArray) => readFiles(filePathsArray)), - mergeMap((csvArray) => loadCSV(csvArray)), - mergeMap(() => filterIIR(1, 30)), + mergeMap((csvArray) => loadCSV(state$.value.pyodide.worker!, csvArray)), + mergeMap(() => filterIIR(state$.value.pyodide.worker!, 1, 30)), map(() => { if (!state$.value.experiment.params?.stimuli) { return {}; } return epochEvents( + state$.value.pyodide.worker!, Object.fromEntries( state$.value.experiment.params?.stimuli.map((stimulus, i) => [ stimulus.title, @@ -126,12 +133,14 @@ const loadCleanedEpochsEpic: Epic< PyodideActionType, PyodideActionType, RootState -> = (action$) => +> = (action$, state$) => action$.pipe( filter(isActionOf(PyodideActions.LoadCleanedEpochs)), pluck('payload'), filter((filePathsArray) => filePathsArray.length >= 1), - map(loadCleanedEpochs), + map((epochsArray) => + loadCleanedEpochs(state$.value.pyodide.worker!, epochsArray) + ), mergeMap(() => of( PyodideActions.GetEpochsInfo(PYODIDE_VARIABLE_NAMES.CLEAN_EPOCHS), @@ -147,9 +156,10 @@ const cleanEpochsEpic: Epic = ( ) => action$.pipe( filter(isActionOf(PyodideActions.CleanEpochs)), - mergeMap(cleanEpochsPlot), + mergeMap(() => cleanEpochsPlot(state$.value.pyodide.worker!)), map(() => saveEpochs( + state$.value.pyodide.worker!, getWorkspaceDir(state$.value.experiment.title), state$.value.experiment.subject ) @@ -165,7 +175,9 @@ const getEpochsInfoEpic: Epic< action$.pipe( filter(isActionOf(PyodideActions.GetEpochsInfo)), pluck('payload'), - mergeMap(requestEpochsInfo), + mergeMap((varName) => + requestEpochsInfo(state$.value.pyodide.worker!, varName) + ), map((epochInfoArray) => epochInfoArray.map((infoObj) => ({ name: Object.keys(infoObj)[0], @@ -182,7 +194,7 @@ const getChannelInfoEpic: Epic< > = (action$, state$) => action$.pipe( filter(isActionOf(PyodideActions.GetChannelInfo)), - mergeMap(requestChannelInfo), + mergeMap(() => requestChannelInfo(state$.value.pyodide.worker!)), map((channelInfoString) => PyodideActions.SetChannelInfo(parseSingleQuoteJSON(channelInfoString)) ) @@ -194,7 +206,7 @@ const loadPSDEpic: Epic = ( ) => action$.pipe( filter(isActionOf(PyodideActions.LoadPSD)), - mergeMap(plotPSD), + mergeMap(() => plotPSD(state$.value.pyodide.worker!)), map(PyodideActions.SetPSDPlot) ); @@ -204,16 +216,17 @@ const loadTopoEpic: Epic = ( ) => action$.pipe( filter(isActionOf(PyodideActions.LoadTopo)), - mergeMap(plotTopoMap), + // mergeMap(plotTopoMap), + mergeMap(() => plotTestPlot(state$.value.pyodide.worker!)), tap((e) => console.log('received topo map: ', e)), mergeMap((topoPlot) => of( - PyodideActions.SetTopoPlot(topoPlot), - PyodideActions.LoadERP( - state$.value.device.deviceType === DEVICES.EMOTIV - ? EMOTIV_CHANNELS[0] - : MUSE_CHANNELS[0] - ) + PyodideActions.SetTopoPlot(topoPlot) + // PyodideActions.LoadERP( + // state$.value.device.deviceType === DEVICES.EMOTIV + // ? EMOTIV_CHANNELS[0] + // : MUSE_CHANNELS[0] + // ) ) ) ); @@ -241,7 +254,7 @@ const loadERPEpic: Epic = ( ); return parseInt(EMOTIV_CHANNELS[0], 10); }), - mergeMap(plotERP), + mergeMap((chanIndex) => plotERP(state$.value.pyodide.worker!, chanIndex)), map(PyodideActions.SetERPPlot) ); diff --git a/app/reducers/pyodideReducer.ts b/app/reducers/pyodideReducer.ts index a3dffc4..9f5c774 100644 --- a/app/reducers/pyodideReducer.ts +++ b/app/reducers/pyodideReducer.ts @@ -24,18 +24,26 @@ export interface PyodideStateType { } | null | undefined; + readonly worker: Worker | null; } -const initialState = { +const initialState: PyodideStateType = { epochsInfo: [], channelInfo: [], psdPlot: null, topoPlot: null, erpPlot: null, + worker: null, }; export default createReducer(initialState, (builder) => builder + .addCase(PyodideActions.SetPyodideWorker, (state, action) => { + return { + ...state, + worker: action.payload, + }; + }) .addCase(PyodideActions.SetEpochInfo, (state, action) => { return { ...state, diff --git a/app/utils/pyodide/index.ts b/app/utils/pyodide/index.ts index c23703b..7b63443 100644 --- a/app/utils/pyodide/index.ts +++ b/app/utils/pyodide/index.ts @@ -2,8 +2,6 @@ import * as path from 'path'; import { readFileSync } from 'fs'; import { formatFilePath } from './functions'; -declare const pyodideWorker: Worker; - // --------------------------------- // This file contains the JS functions that allow the app to access python-wasm through pyodide // These functions wrap the python strings defined in the @@ -11,27 +9,31 @@ declare const pyodideWorker: Worker; // ----------------------------- // Imports and Utility functions -export const loadPyodide = () => { - return new Worker('./utils/pyodide/webworker.js'); +export const loadPyodide = async () => { + const freshWorker = await new Worker('./utils/pyodide/webworker.js'); + return freshWorker; }; -export const loadUtils = async () => - pyodideWorker.postMessage({ +export const loadUtils = async (worker: Worker) => + worker.postMessage({ data: readFileSync(path.join(__dirname, '/utils/pyodide/utils.py'), 'utf8'), }); -export const loadCSV = async (csvArray: Array) => { +export const loadCSV = async (worker: Worker, csvArray: Array) => { // TODO: Pass attached variable name as parameter to load_data // @ts-expect-error window.csvArray = csvArray; - await pyodideWorker.postMessage({ data: `raw = load_data()` }); + await worker.postMessage({ data: `raw = load_data()` }); }; // --------------------------- // MNE-Related Data Processing -export const loadCleanedEpochs = async (epochsArray: string[]) => { - await pyodideWorker.postMessage({ +export const loadCleanedEpochs = async ( + worker: Worker, + epochsArray: string[] +) => { + await worker.postMessage({ data: [ `clean_epochs = concatenate_epochs([read_epochs(file) for file in ${epochsArray}])`, `conditions = OrderedDict({key: [value] for (key, value) in clean_epochs.event_id.items()})`, @@ -40,18 +42,23 @@ export const loadCleanedEpochs = async (epochsArray: string[]) => { }; // NOTE: this command includes a ';' to prevent returning data -export const filterIIR = async (lowCutoff: number, highCutoff: number) => - pyodideWorker.postMessage({ +export const filterIIR = async ( + worker: Worker, + lowCutoff: number, + highCutoff: number +) => + worker.postMessage({ data: `raw.filter(${lowCutoff}, ${highCutoff}, method='iir');`, }); export const epochEvents = async ( + worker: Worker, eventIDs: { [k: string]: number }, tmin: number, tmax: number, reject?: string[] | 'None' ) => - pyodideWorker.postMessage({ + worker.postMessage({ data: [ `event_id = ${JSON.stringify(eventIDs)}`, `tmin=${tmin}`, @@ -67,49 +74,66 @@ export const epochEvents = async ( ].join('\n'), }); -export const requestEpochsInfo = async (variableName: string) => { - const pyodideReturn = await pyodideWorker.postMessage({ +export const requestEpochsInfo = async ( + worker: Worker, + variableName: string +) => { + const pyodideReturn = await worker.postMessage({ data: `get_epochs_info(${variableName})`, }); return pyodideReturn; }; -export const requestChannelInfo = async () => - pyodideWorker.postMessage({ +export const requestChannelInfo = async (worker: Worker) => + worker.postMessage({ data: `[ch for ch in clean_epochs.ch_names if ch != 'Marker']`, }); // ----------------------------- // Plot functions -export const cleanEpochsPlot = async () => { +export const cleanEpochsPlot = async (worker: Worker) => { // TODO: Figure out how to get image results from pyodide - await pyodideWorker.postMessage({ + await worker.postMessage({ data: `raw_epochs.plot(scalings='auto', n_epochs=6, title="Clean Data", events=None)`, }); }; -export const plotPSD = async () => { +export const plotPSD = async (worker: Worker) => { // TODO: Figure out how to get image results from pyodide - return pyodideWorker.postMessage({ data: `raw.plot_psd(fmin=1, fmax=30)` }); + return worker.postMessage({ data: `raw.plot_psd(fmin=1, fmax=30)` }); }; -export const plotTopoMap = async () => { +export const plotTopoMap = async (worker: Worker) => { // TODO: Figure out how to get image results from pyodide - return pyodideWorker.postMessage({ + return worker.postMessage({ data: `plot_topo(clean_epochs, conditions)`, }); }; -export const plotERP = async (channelIndex: number) => { - return pyodideWorker.postMessage({ +export const plotTestPlot = async (worker: Worker | null) => { + if (!worker) { + return; + } + // TODO: Figure out how to get image results from pyodide + return worker.postMessage({ + data: `plt.plot([1,2,3,4])`, + }); +}; + +export const plotERP = async (worker: Worker, channelIndex: number) => { + return worker.postMessage({ data: `X, y = plot_conditions(clean_epochs, ch_ind=${channelIndex}, conditions=conditions, ci=97.5, n_boot=1000, title='', diff_waveform=None)`, }); }; -export const saveEpochs = (workspaceDir: string, subject: string) => - pyodideWorker.postMessage({ +export const saveEpochs = ( + worker: Worker, + workspaceDir: string, + subject: string +) => + worker.postMessage({ data: `raw_epochs.save(${formatFilePath( path.join( workspaceDir, diff --git a/app/utils/pyodide/webworker.js b/app/utils/pyodide/webworker.js index d913c0d..a28511b 100644 --- a/app/utils/pyodide/webworker.js +++ b/app/utils/pyodide/webworker.js @@ -7,8 +7,8 @@ importScripts('./src/pyodide/pyodide.js'); async function loadPyodideAndPackages() { - await loadPyodide({ indexURL: './src/pyodide/' }); - await self.pyodide.loadPackage(['matplotlib', 'mne', 'pandas']); + self.pyodide = await loadPyodide({ indexURL: './src/pyodide/' }); + await self.pyodide.loadPackage(['numpy']); } let pyodideReadyPromise = loadPyodideAndPackages();