Skip to content

Commit

Permalink
fix LSL stream timing
Browse files Browse the repository at this point in the history
  • Loading branch information
timonmerk committed Nov 18, 2024
1 parent c72b1fc commit 5e67151
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 159 deletions.
219 changes: 112 additions & 107 deletions examples/plot_7_lsl_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,114 +11,119 @@
# %%
from matplotlib import pyplot as plt
import py_neuromodulation as nm
import asyncio
from multiprocessing import freeze_support

# %%
# Let’s get the example data from the provided BIDS dataset and create the channels DataFrame.

(
RUN_NAME,
PATH_RUN,
PATH_BIDS,
PATH_OUT,
datatype,
) = nm.io.get_paths_example_data()

(
raw,
data,
sfreq,
line_noise,
coord_list,
coord_names,
) = nm.io.read_BIDS_data(PATH_RUN=PATH_RUN)

channels = nm.utils.set_channels(
ch_names=raw.ch_names,
ch_types=raw.get_channel_types(),
reference="default",
bads=raw.info["bads"],
new_names="default",
used_types=("ecog", "dbs", "seeg"),
target_keywords=["MOV_RIGHT"],
)

# %%
# Playing the Data
# ----------------
#
# Now we need our data to be represeted in the LSL stream.
# For this example an mne_lsl.Player is utilized, which is playing our earlier
# recorded data. However, you could make use of any LSL source (live or
# offline).
# If you want to bind your own data source, make sure to specify the
# necessary parameters (data type, type, name) accordingly.
# If you are unsure about the parameters of your data source you can
# always search for available lsl streams.
#

settings = nm.NMSettings.get_fast_compute()

player = nm.stream.LSLOfflinePlayer(raw=raw, stream_name="example_stream")

player.start_player(chunk_size=30)
# %%
# Creating the LSLStream object
# -----------------------------
#
# Next let’s create a Stream analog to the First Demo’s example However as
# we run the stream, we will set the *lsl-stream* value to True and pass
# the stream name we earlier declared when initializing the player object.

settings.features.welch = False
settings.features.fft = True
settings.features.bursts = False
settings.features.sharpwave_analysis = False
settings.features.coherence = False

# %%
stream = nm.Stream(
sfreq=sfreq,
channels=channels,
settings=settings,
coord_list=coord_list,
verbose=True,
line_noise=line_noise,
)
# %%
# We then simply have to set the `stream_lsl` parameter to be `True` and specify the `stream_lsl_name`.

features = stream.run(
is_stream_lsl=True,
plot_lsl=False,
stream_lsl_name="example_stream",
out_dir=PATH_OUT,
experiment_name=RUN_NAME,
)

# %%
# We can then look at the computed features and check if the streamed data was processed correctly.
# This can be verified by the time label:

plt.plot(features.time, features.MOV_RIGHT)


######################################################################
# Feature Analysis of Movement
# ----------------------------
# We can now check the movement averaged features of an ECoG channel.
# Note that the path was here adapted to be documentation build compliant.


feature_reader = nm.analysis.FeatureReader(feature_dir=PATH_OUT, feature_file=RUN_NAME)
feature_reader.label_name = "MOV_RIGHT"
feature_reader.label = feature_reader.feature_arr["MOV_RIGHT"]

feature_reader.plot_target_averaged_channel(
ch="ECOG_RIGHT_0",
list_feature_keywords=None,
epoch_len=4,
threshold=0.5,
ytick_labelsize=7,
figsize_x=12,
figsize_y=12,
)
if __name__ == "__main__":
freeze_support()
(
RUN_NAME,
PATH_RUN,
PATH_BIDS,
PATH_OUT,
datatype,
) = nm.io.get_paths_example_data()

(
raw,
data,
sfreq,
line_noise,
coord_list,
coord_names,
) = nm.io.read_BIDS_data(PATH_RUN=PATH_RUN)

channels = nm.utils.set_channels(
ch_names=raw.ch_names,
ch_types=raw.get_channel_types(),
reference="default",
bads=raw.info["bads"],
new_names="default",
used_types=("ecog", "dbs", "seeg"),
target_keywords=["MOV_RIGHT"],
)

# %%
# Playing the Data
# ----------------
#
# Now we need our data to be represeted in the LSL stream.
# For this example an mne_lsl.Player is utilized, which is playing our earlier
# recorded data. However, you could make use of any LSL source (live or
# offline).
# If you want to bind your own data source, make sure to specify the
# necessary parameters (data type, type, name) accordingly.
# If you are unsure about the parameters of your data source you can
# always search for available lsl streams.
#

settings = nm.NMSettings.get_fast_compute()

player = nm.stream.LSLOfflinePlayer(raw=raw, stream_name="example_stream")

player.start_player(chunk_size=30, n_repeat=5999999)
import time
time.sleep(5)
# %%
# Creating the LSLStream object
# -----------------------------
#
# Next let’s create a Stream analog to the First Demo’s example However as
# we run the stream, we will set the *lsl-stream* value to True and pass
# the stream name we earlier declared when initializing the player object.

settings.features.welch = False
settings.features.fft = True
settings.features.bursts = False
settings.features.sharpwave_analysis = False
settings.features.coherence = False

# %%
stream = nm.Stream(
sfreq=sfreq,
channels=channels,
settings=settings,
coord_list=coord_list,
verbose=True,
line_noise=line_noise,
)
# %%
# We then simply have to set the `stream_lsl` parameter to be `True` and specify the `stream_lsl_name`.

features = asyncio.run(stream.run(
is_stream_lsl=True,
stream_lsl_name="example_stream",
out_dir=PATH_OUT,
experiment_name=RUN_NAME,
))

# %%
# We can then look at the computed features and check if the streamed data was processed correctly.
# This can be verified by the time label:

plt.plot(features.time, features.MOV_RIGHT)


######################################################################
# Feature Analysis of Movement
# ----------------------------
# We can now check the movement averaged features of an ECoG channel.
# Note that the path was here adapted to be documentation build compliant.


feature_reader = nm.analysis.FeatureReader(feature_dir=PATH_OUT, feature_file=RUN_NAME)
feature_reader.label_name = "MOV_RIGHT"
feature_reader.label = feature_reader.feature_arr["MOV_RIGHT"]

feature_reader.plot_target_averaged_channel(
ch="ECOG_RIGHT_0",
list_feature_keywords=None,
epoch_len=4,
threshold=0.5,
ytick_labelsize=7,
figsize_x=12,
figsize_y=12,
)
6 changes: 5 additions & 1 deletion gui_dev/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ dist-ssr
*.local

# Dev-scripts
gui_startup.py
gui_startup.py

# Remove once front was built
py_neuromodulation/gui/frontend/*

Binary file modified gui_dev/bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion gui_dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"babel-plugin-react-compiler": "latest",
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jsdoc": "^50.4.3",
"eslint-plugin-jsdoc": "^50.5.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-compiler": "latest",
"eslint-plugin-react-hooks": "^4.6.2",
Expand Down
4 changes: 3 additions & 1 deletion gui_dev/src/components/HeatmapGraph.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export const HeatmapGraph = () => {
if (!graphData || !selectedChannel || features.length === 0 || selectedFeatures.length === 0) return;

// TODO: Always data in ms? (Time conversion here always necessary?)
// Timon: yes, let's define that the stream's time is always in ms
let timestamp = graphData.time;
if (timestamp === undefined) {
timestamp = (Date.now() - hasInitialized.current) / 1000;
Expand Down Expand Up @@ -141,6 +142,7 @@ export const HeatmapGraph = () => {

const currentTime = timestamp;
const minTime = currentTime - maxTimeWindow; // TODO: What should be the visible window frame? adjustable? 10s?
// Timon: Would be amazing if it's adjustable

const validIndices = x.reduce((indices, time, index) => {
if (time >= minTime) {
Expand Down Expand Up @@ -221,7 +223,7 @@ export const HeatmapGraph = () => {
<FormControlLabel
key={channel.id || index}
value={channel.name}
control={<Radio />} // TODO: Should we make multiple selectable?
control={<Radio />} // TODO: Should we make multiple selectable? // Timon: No, let's keep with one
label={channel.name}
/>
))}
Expand Down
19 changes: 18 additions & 1 deletion gui_dev/src/components/utils.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";

import { TextField } from "@mui/material";
/**
* @param {Object} props
* @param {string} props.href - The URL to link to
Expand Down Expand Up @@ -41,3 +41,20 @@ export const getChannelAndFeature = (availableChannels, keystr) => {
return { channelName, featureName};
};

export const MyTextField = ({ label, value, onChange }) => (
<TextField
label={label}
variant="outlined"
size="small"
fullWidth
sx={{
marginBottom: 2,
backgroundColor: "#616161",
color: "#f4f4f4",
}}
InputLabelProps={{ style: { color: "#cccccc" } }}
InputProps={{ style: { color: "#f4f4f4" } }}
value={value}
onChange={onChange}
/>
);
29 changes: 11 additions & 18 deletions gui_dev/src/pages/SourceSelection/StreamParameters.jsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,7 @@
import { TextField } from "@mui/material";
import { useSessionStore } from "@/stores";
import { TitledBox } from "@/components";

const MyTextField = ({ label, value, onChange }) => (
<TextField
label={label}
variant="outlined"
size="small"
fullWidth
sx={{
marginBottom: 2,
backgroundColor: "#616161",
color: "#f4f4f4",
}}
InputLabelProps={{ style: { color: "#cccccc" } }}
InputProps={{ style: { color: "#f4f4f4" } }}
value={value}
onChange={onChange}
/>
);
import { MyTextField } from "@/components/utils";

export const StreamParameters = () => {
const streamParameters = useSessionStore((state) => state.streamParameters);
Expand Down Expand Up @@ -51,6 +34,16 @@ export const StreamParameters = () => {
value={streamParameters.samplingRateFeatures}
onChange={(event) => handleOnChange(event, "samplingRateFeatures")}
/>
<MyTextField
label="experiment name"
value={streamParameters.experimentName}
onChange={(event) => handleOnChange(event, "experimentName")}
/>
<MyTextField
label="output directory"
value={streamParameters.outputDirectory}
onChange={(event) => handleOnChange(event, "outputDirectory")}
/>
</TitledBox>
);
};
6 changes: 6 additions & 0 deletions gui_dev/src/stores/sessionStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export const useSessionStore = createPersistStore("session", (set, get) => ({
lineNoise: 50,
samplingRateFeatures: 10,
allValid: false,
experimentName: "sub",
outputDirectory: "default",
},

setSourceType: (type) => set({ sourceType: type }),
Expand Down Expand Up @@ -128,6 +130,8 @@ export const useSessionStore = createPersistStore("session", (set, get) => ({
file_path: get().fileSource.path,
sampling_rate_features: get().streamParameters.samplingRateFeatures,
line_noise: get().streamParameters.lineNoise,
experimentName: get().streamParameters.experimentName,
outputDirectory: get().streamParameters.outputDirectory,
}),
});

Expand Down Expand Up @@ -168,6 +172,8 @@ export const useSessionStore = createPersistStore("session", (set, get) => ({
stream_name: lslSource.availableStreams[0].name,
sampling_rate_features: streamParameters.samplingRate,
line_noise: streamParameters.lineNoise,
experimentName: streamParameters.experimentName,
outputDirectory: streamParameters.outputDirectory,
}),
});

Expand Down
Loading

0 comments on commit 5e67151

Please sign in to comment.