Merge pull request #110 from kylemath/evoked

[ready to deploy] - Markers locked to P5 stimuli logged into Raw csv file
Esse commit está contido em:
Kyle Mathewson
2020-01-06 01:51:09 -07:00
commit de GitHub
10 arquivos alterados com 407 adições e 7 exclusões
+24 -4
Ver Arquivo
@@ -1,4 +1,4 @@
import React, { useState, useCallback } from "react";
import React, { useState, useCallback } from "react";
import { MuseClient } from "muse-js";
import { Select, Card, Stack, Button, ButtonGroup } from "@shopify/polaris";
@@ -15,9 +15,10 @@ import * as funSpectra from "./components/EEGEduSpectra/EEGEduSpectra";
import * as funBands from "./components/EEGEduBands/EEGEduBands";
import * as funAnimate from "./components/EEGEduAnimate/EEGEduAnimate";
import * as funSpectro from "./components/EEGEduSpectro/EEGEduSpectro";
import * as funAlpha from "./components/EEGEduAlpha/EEGEduAlpha"
import * as funSsvep from "./components/EEGEduSsvep/EEGEduSsvep"
import * as funPredict from "./components/EEGEduPredict/EEGEduPredict"
import * as funAlpha from "./components/EEGEduAlpha/EEGEduAlpha";
import * as funSsvep from "./components/EEGEduSsvep/EEGEduSsvep";
import * as funEvoked from "./components/EEGEduEvoked/EEGEduEvoked";
import * as funPredict from "./components/EEGEduPredict/EEGEduPredict";
const intro = translations.types.intro;
const heartRaw = translations.types.heartRaw;
@@ -29,6 +30,7 @@ const animate = translations.types.animate;
const spectro = translations.types.spectro;
const alpha = translations.types.alpha;
const ssvep = translations.types.ssvep;
const evoked = translations.types.evoked;
const predict = translations.types.predict;
export function PageSwitcher() {
@@ -44,6 +46,7 @@ export function PageSwitcher() {
const [spectroData, setSpectroData] = useState(emptyChannelData);
const [alphaData, setAlphaData] = useState(emptyChannelData);
const [ssvepData, setSsvepData] = useState(emptyChannelData);
const [evokedData, setEvokedData] = useState(emptyChannelData);
const [predictData, setPredictData] = useState(emptyChannelData);
// pipe settings
@@ -57,6 +60,7 @@ export function PageSwitcher() {
const [spectroSettings, setSpectroSettings] = useState(funSpectro.getSettings);
const [alphaSettings, setAlphaSettings] = useState(funAlpha.getSettings);
const [ssvepSettings, setSsvepSettings] = useState(funSsvep.getSettings);
const [evokedSettings, setEvokedSettings] = useState(funEvoked.getSettings);
const [predictSettings, setPredictSettings] = useState(funPredict.getSettings);
// connection status
@@ -79,6 +83,7 @@ export function PageSwitcher() {
if (window.subscriptionSpectro) window.subscriptionSpectro.unsubscribe();
if (window.subscriptionAlpha) window.subscriptionAlpha.unsubscribe();
if (window.subscriptionSsvep) window.subscriptionSsvep.unsubscribe();
if (window.subscriptionEvoked) window.subscriptionEvoked.unsubscribe();
if (window.subscriptionPredict) window.subscriptionPredict.unsubscribe();
subscriptionSetup(value);
@@ -104,6 +109,7 @@ export function PageSwitcher() {
{ label: spectro, value: spectro },
{ label: alpha, value: alpha },
{ label: ssvep, value: ssvep },
{ label: evoked, value: evoked },
{ label: predict, value: predict }
];
@@ -119,6 +125,7 @@ export function PageSwitcher() {
funSpectro.buildPipe(spectroSettings);
funAlpha.buildPipe(alphaSettings);
funSsvep.buildPipe(ssvepSettings);
funEvoked.buildPipe(evokedSettings);
funPredict.buildPipe(predictSettings);
}
@@ -154,6 +161,9 @@ export function PageSwitcher() {
case ssvep:
funSsvep.setup(setSsvepData, ssvepSettings);
break;
case evoked:
funEvoked.setup(setEvokedData, evokedSettings);
break;
case predict:
funPredict.setup(setPredictData, predictSettings);
break;
@@ -236,6 +246,10 @@ export function PageSwitcher() {
return (
funSsvep.renderSliders(setSsvepData, setSsvepSettings, status, ssvepSettings)
);
case evoked:
return (
funEvoked.renderSliders(setEvokedData, setEvokedSettings, status, evokedSettings)
);
case predict:
return (
funPredict.renderSliders(setPredictData, setPredictSettings, status, predictSettings)
@@ -266,6 +280,8 @@ export function PageSwitcher() {
return <funAlpha.renderModule data={alphaData} />;
case ssvep:
return <funSsvep.renderModule data={ssvepData} />;
case evoked:
return <funEvoked.renderModule data={evokedData} />;
case predict:
return <funPredict.renderModule data={predictData} />;
default:
@@ -307,6 +323,10 @@ export function PageSwitcher() {
return (
funSsvep.renderRecord(recordPopChange, recordPop, status, ssvepSettings, recordTwoPopChange, recordTwoPop)
)
case evoked:
return (
funEvoked.renderRecord(recordPopChange, recordPop, status, evokedSettings)
)
case predict:
return (
funPredict.renderRecord(status)
@@ -36,7 +36,8 @@ export function getSettings () {
interval: 16,
bins: 256,
duration: 128,
srate: 256
srate: 256,
name: 'Animate'
}
};
@@ -0,0 +1,300 @@
import React from "react";
import { catchError, multicast } from "rxjs/operators";
import { Subject } from "rxjs";
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
import { saveAs } from 'file-saver';
import { take } from "rxjs/operators";
import { zipSamples } from "muse-js";
import {
bandpassFilter,
epoch
} from "@neurosity/pipes";
import { chartStyles } from "../chartOptions";
import * as generalTranslations from "../translations/en";
import * as specificTranslations from "./translations/en";
import { generateXTics, standardDeviation } from "../../utils/chartUtils";
import P5Wrapper from 'react-p5-wrapper';
import sketchEvoked from './sketchEvoked'
export function getSettings () {
return {
cutOffLow: 2,
cutOffHigh: 20,
nbChannels: 4,
interval: 1,
srate: 256,
duration: 1,
name: 'Evoked'
}
};
export function buildPipe(Settings) {
if (window.subscriptionEvoked) window.subscriptionEvoked.unsubscribe();
window.pipeEvoked$ = null;
window.multicastEvoked$ = null;
window.subscriptionEvoked = null;
// Build Pipe
window.pipeEvoked$ = zipSamples(window.source.eegReadings$).pipe(
bandpassFilter({
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
nbChannels: Settings.nbChannels }),
epoch({
duration: Settings.duration,
interval: Settings.interval,
samplingRate: Settings.srate
}),
catchError(err => {
console.log(err);
})
);
window.multicastEvoked$ = window.pipeEvoked$.pipe(
multicast(() => new Subject())
);
}
export function setup(setData, Settings) {
console.log("Subscribing to " + Settings.name);
if (window.multicastEvoked$) {
window.subscriptionEvoked = window.multicastEvoked$.subscribe(data => {
setData(evokedData => {
Object.values(evokedData).forEach((channel, index) => {
if (index < 4) {
channel.datasets[0].data = data.data[index];
channel.xLabels = generateXTics(Settings.srate, Settings.duration);
channel.datasets[0].qual = standardDeviation(data.data[index])
}
});
return {
ch0: evokedData.ch0,
ch1: evokedData.ch1,
ch2: evokedData.ch2,
ch3: evokedData.ch3
};
});
});
window.multicastEvoked$.connect();
console.log("Subscribed to Evoked");
}
}
export function renderModule(channels) {
function renderCharts() {
return null
}
return (
<Card title={specificTranslations.title}>
<Card.Section>
<Stack>
<TextContainer>
<p>{specificTranslations.description}</p>
</TextContainer>
</Stack>
</Card.Section>
<Card.Section>
<div style={chartStyles.wrapperStyle.style}>{renderCharts()}</div>
</Card.Section>
</Card>
);
}
export function renderSliders(setData, setSettings, status, Settings) {
function resetPipeSetup(value) {
buildPipe(Settings);
setup(setData, Settings)
}
function handleIntervalRangeSliderChange(value) {
setSettings(prevState => ({...prevState, interval: value}));
resetPipeSetup();
}
function handleCutoffLowRangeSliderChange(value) {
setSettings(prevState => ({...prevState, cutOffLow: value}));
resetPipeSetup();
}
function handleCutoffHighRangeSliderChange(value) {
setSettings(prevState => ({...prevState, cutOffHigh: value}));
resetPipeSetup();
}
function handleDurationRangeSliderChange(value) {
setSettings(prevState => ({...prevState, duration: value}));
resetPipeSetup();
}
return (
<Card title={Settings.name + ' Settings'} sectioned>
<RangeSlider
disabled={status === generalTranslations.connect}
min={128} step={128} max={4096}
label={'Epoch duration (Sampling Points): ' + Settings.duration}
value={Settings.duration}
onChange={handleDurationRangeSliderChange}
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={10} step={1} max={Settings.duration}
label={'Sampling points between epochs onsets: ' + Settings.interval}
value={Settings.interval}
onChange={handleIntervalRangeSliderChange}
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={.01} step={.5} max={Settings.cutOffHigh - .5}
label={'Cutoff Frequency Low: ' + Settings.cutOffLow + ' Hz'}
value={Settings.cutOffLow}
onChange={handleCutoffLowRangeSliderChange}
/>
<RangeSlider
disabled={status === generalTranslations.connect}
min={Settings.cutOffLow + .5} step={.5} max={Settings.srate/2}
label={'Cutoff Frequency High: ' + Settings.cutOffHigh + ' Hz'}
value={Settings.cutOffHigh}
onChange={handleCutoffHighRangeSliderChange}
/>
</Card>
)
}
export function renderRecord(recordPopChange, recordPop, status, Settings) {
return (
<Card title={'Run ERP experiment'} sectioned>
<Card.Section>
<p>
{"Clicking this button will begin the experiment so check your data quality on the raw module first. "}
{"A window will pop up when you click the button and a series of circles will appear. Stare at the cross in the center. "}
{"There will be red and blue circles, ignore the blue ones."}
{"Whenever you see a red circle, as fast as you can either press spacebar on a keyboard, or tap the touchscreen on a tablet or phone. "}
{"This entire time the eeg data will be saved along with a column indicating which target was on the screen, and another for the responses. "}
{"The task will continue for a few minutes and once it is finished a .csv file will automatically download. "}
{"This .csv file has a row for each time point, a column for each electrode, and the columns indicating when targets appeared, and when responses were made. "}
{"It saves a marker of 20 when the target is on, a marker of 10 when the blue standards are on, and a peak when the spacebar or touchscreen are pressed, here you can see those synced with an eeg channel: "}
</p>
<p>
<img
src={ require("./dataExample.png")}
alt="dataExample"
width="100%"
height="auto"
></img>
</p>
</Card.Section>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(Settings);
recordPopChange();
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Run oddball experiment'}
</Button>
</ButtonGroup>
<Modal
open={recordPop}
onClose={recordPopChange}
title={"Press Spacebar or tap screen when you see RED circle"}
>
<Modal.Section>
<Card.Section>
<P5Wrapper sketch={sketchEvoked} />
</Card.Section>
<TextContainer>
<p>
Your data is currently recording,
once complete it will be downloaded as a .csv file
and can be opened with your favorite spreadsheet program.
Close this window once the download completes.
</p>
</TextContainer>
</Modal.Section>
</Modal>
</Stack>
</Card>
)
}
function saveToCSV(Settings) {
const numSamplesToSave = 10000;
console.log('Saving ' + numSamplesToSave + ' samples...');
var localObservable$ = null;
const dataToSave = [];
window.marker = 0;
window.responseMarker = 0;
window.touchMarker = 0;
console.log('making ' + Settings.name + ' headers')
// for each module subscribe to multicast and make header
// take one sample from selected observable object for headers
localObservable$ = window.multicastEvoked$.pipe(
take(1)
);
//take one sample to get header info
localObservable$.subscribe({
next(x) {
dataToSave.push(
"Timestamp (ms),",
"Marker,",
"SpaceBar,",
"TouchMarker,",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch0_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch1_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch2_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch3_" + f + "ms"}) + ",",
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "chAux_" + f + "ms"}) + ",",
"info",
"\n"
);
}
});
// put selected observable object into local and start taking samples
localObservable$ = window.multicastEvoked$.pipe(
take(numSamplesToSave)
);
// now with header in place subscribe to each epoch and log it
localObservable$.subscribe({
next(x) {
dataToSave.push(
Date.now() + "," +
window.marker + "," +
window.responseMarker + "," +
window.touchMarker + "," +
Object.values(x).join(",") + "\n");
// logging is useful for debugging -yup
// console.log(x);
},
error(err) { console.log(err); },
complete() {
console.log('Trying to save')
var blob = new Blob(
dataToSave,
{type: "text/plain;charset=utf-8"}
);
saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv");
console.log('Completed');
}
});
}
Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 224 KiB

@@ -0,0 +1,64 @@
export default function sketchEvoked (p) {
let x = 0;
let thisRand = 0.5; //for random choice of target type
let targProp = 0.25;
let isTarget = false;
p.setup = function () {
p.createCanvas(300, 300);
p.frameRate(30);
p.noStroke();
};
p.windowResized = function() {
p.createCanvas(300, 300);
}
p.touchStarted = function() {
if (window.touchMarker === 0) {
window.touchMarker = 255;
} else {
window.touchMarker = 0;
}
}
p.draw = function () {
if (p.keyIsPressed === true) {
window.responseMarker = p.keyCode;
} else {
window.responseMarker = 0;
}
p.background(255);
x = x+1;
// var num = int(random(0, 11));
if (x % 20 === 0) { // When a target shown (every ith frame for now)
thisRand = p.random();
if (thisRand < targProp) { // targets 20% of the time
isTarget = true;
} else {
isTarget = false;
}
if (isTarget) {
p.fill(250, 150, 150);
window.marker = 20;
} else {
p.fill(150,150,250);
window.marker = 10;
}
} else { // during time between targets
p.fill(255, 255, 255);
window.marker = 0;
}
p.ellipse(p.width/2, p.height/2, 300);
p.fill(255,0,0);
p.text("+", p.width/2, p.height/2);
}
};
@@ -0,0 +1,12 @@
{
"title": "Stimulus Evoked Event-related potential (ERP)",
"description": [
"The electrical activity evoked by individual presentations of stimuli does create small changes in electrical voltage. ",
"These changes are, however, very small, about 1-10 microVolts (\u03BCV), and the noise from other things like eye movements and muscles is sometimes 10x as large. ",
"This noise can be mitigated by averaging this evoked activity over repeated presentations of the same stimulus. ",
"This average voltage in response to a stimulus is called an Event-related potential (ERP) or evoked potential (EP). ",
"Here..."
],
"xlabel": "Time (ms)",
"ylabel": "Votage (\u03BCV)"
}
@@ -8,6 +8,6 @@
"Along the horizontal axis is the beats per minute, or the frequency of the peaks in your ECG. ",
"The vertical y-axis shows the power of the rhythms in the data at each frequency, or how large the changes are between peak and through of the oscillations. "
],
"xlabel": "Frequency (Hz)",
"xlabel": "Heart Frequency (BPM)",
"ylabel": "Power (\u03BCV\u00B2)"
}
@@ -4,6 +4,7 @@ export default function sketchFlash (p) {
p.setup = function () {
p.createCanvas(300, 300);
p.frameRate(30);
};
p.windowResized = function() {
@@ -4,6 +4,7 @@ export default function sketchFlash (p) {
p.setup = function () {
p.createCanvas(300, 300);
p.frameRate(30);
};
p.windowResized = function() {
@@ -11,6 +11,7 @@
"spectro": "8. Spectrogram (spectra over time)",
"alpha": "9. Eyes open vs. Eyes closed Experiment",
"ssvep": "10. Steady-State Visual Evoked Potential (SSVEP) Experiment",
"predict": "11. Predict brain states with a trained classifier"
"evoked": "11. Stimulus Evoked Event-related potential (ERP)",
"predict": "12. Predict brain states with a trained classifier"
}
}