everything ready for evoked dev in new module

Esse commit está contido em:
Kyle Mathewson
2020-01-05 01:56:05 -07:00
commit 5ae8742861
7 arquivos alterados com 403 adições e 8 exclusões
+25 -5
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,13 +60,14 @@ 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
const [status, setStatus] = useState(generalTranslations.connect);
// for picking a new module
const [selected, setSelected] = useState(intro);
const [selected, setSelected] = useState(evoked);
const handleSelectChange = useCallback(value => {
setSelected(value);
@@ -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,328 @@
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 { channelNames } from "muse-js";
import { Line } from "react-chartjs-2";
import { zipSamples } from "muse-js";
import {
bandpassFilter,
epoch
} from "@neurosity/pipes";
import { chartStyles, generalOptions } 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: 1024,
srate: 256,
duration: 1024,
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 Object.values(channels.data).map((channel, index) => {
const options = {
...generalOptions,
scales: {
xAxes: [
{
scaleLabel: {
...generalOptions.scales.xAxes[0].scaleLabel,
labelString: specificTranslations.xlabel
}
}
],
yAxes: [
{
scaleLabel: {
...generalOptions.scales.yAxes[0].scaleLabel,
labelString: specificTranslations.ylabel
},
ticks: {
max: 300,
min: -300
}
}
]
},
elements: {
line: {
borderColor: 'rgba(' + channel.datasets[0].qual*10 + ', 128, 128)',
fill: false
},
point: {
radius: 0
}
},
animation: {
duration: 0
},
title: {
...generalOptions.title,
text: generalTranslations.channel + channelNames[index] + ' --- SD: ' + channel.datasets[0].qual
}
};
return (
<Card.Section key={"Card_" + index}>
<Line key={"Line_" + index} data={channel} options={options} />
</Card.Section>
);
});
}
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={'Record ' + Settings.name + ' Data'} sectioned>
<Card.Section>
<p>
{"When you are recording Evoked data it is recommended you set the "}
{"number of sampling points between epochs onsets to be equal to the epoch duration. "}
{"This will ensure that consecutive rows of your output file are not overlapping in time."}
{"It will make the live plots appear more choppy."}
</p>
</Card.Section>
<Stack>
<ButtonGroup>
<Button
onClick={() => {
saveToCSV(Settings);
recordPopChange();
}}
primary={status !== generalTranslations.connect}
disabled={status === generalTranslations.connect}
>
{'Save to CSV'}
</Button>
</ButtonGroup>
<Modal
open={recordPop}
onClose={recordPopChange}
title={"Recording Data"}
>
<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 = 50;
console.log('Saving ' + numSamplesToSave + ' samples...');
var localObservable$ = null;
const dataToSave = [];
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),",
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() + "," + 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');
}
});
}
@@ -0,0 +1,33 @@
export default function sketchEvoked (p) {
let x = 0;
p.setup = function () {
p.createCanvas(300, 300);
};
p.windowResized = function() {
p.createCanvas(300, 300);
}
p.mousePressed = function () {
p.background(256);
}
p.draw = function () {
p.background(255);
x = x+1;
if (x % 11 === 0) {
p.fill(0, 0, 0);
} else {
p.fill(255, 255, 255);
}
p.noStroke();
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)"
}
@@ -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"
}
}