diff --git a/eegnb/experiments/__init__.py b/eegnb/experiments/__init__.py index 9306e07b..49d0af05 100644 --- a/eegnb/experiments/__init__.py +++ b/eegnb/experiments/__init__.py @@ -1,7 +1,7 @@ from .visual_n170.n170 import VisualN170 from .visual_p300.p300 import VisualP300 from .visual_ssvep.ssvep import VisualSSVEP - +from .semantic_n400.n400 import TextN400 # PTB does not yet support macOS Apple Silicon, # this experiment needs to run as i386 if on macOS. import sys diff --git a/eegnb/experiments/semantic_n400/n400.py b/eegnb/experiments/semantic_n400/n400.py new file mode 100644 index 00000000..5776925e --- /dev/null +++ b/eegnb/experiments/semantic_n400/n400.py @@ -0,0 +1,139 @@ +""" eeg-notebooks/eegnb/experiments/semantic_n400/n400.py """ + +import os +from time import time +from glob import glob +from random import choice, shuffle +from psychopy import visual, core, event + +from eegnb.devices.eeg import EEG +from eegnb.stimuli import N400_TEXT +from eegnb.experiments import Experiment +from typing import Optional + +import pandas as pd +import numpy as np + + +class TextN400(Experiment.BaseExperiment): + + def __init__(self, duration=120, eeg: Optional[EEG]=None, save_fn=None, + n_trials=200, iti=0.5, soa=0.5, jitter=0.2): + + + exp_name = "Text N400" + + super(TextN400, self).__init__(exp_name, duration, eeg, save_fn, n_trials, iti, soa, jitter) + + + + self.markernames = { + 'congruent': 1, + 'incongruent': 2 + } + + def load_stimulus(self): + """ + Loads the text stimuli (congruent and incongruent sentences/words) + from the specified folders. + """ + + text_stim_path = N400_TEXT + + + congruent_files = glob(os.path.join(text_stim_path, "congruent", "*.txt")) + self.congruent_stims = [] + for f in congruent_files: + with open(f, 'r') as file: + + self.congruent_stims.extend([line.strip() for line in file if line.strip()]) + + + incongruent_files = glob(os.path.join(text_stim_path, "incongruent", "*.txt")) + self.incongruent_stims = [] + for f in incongruent_files: + with open(f, 'r') as file: + self.incongruent_stims.extend([line.strip() for line in file if line.strip()]) + + + num_congruent = len(self.congruent_stims) + num_incongruent = len(self.incongruent_stims) + + + total_stims_available = num_congruent + num_incongruent + if total_stims_available == 0: + raise ValueError("No stimulus text files found in the congruent or incongruent folders!") + + + ratio_congruent = num_congruent / total_stims_available + ratio_incongruent = num_incongruent / total_stims_available + + trials_to_use_congruent = min(num_congruent, int(self.n_trials * ratio_congruent)) + trials_to_use_incongruent = min(num_incongruent, int(self.n_trials * ratio_incongruent)) + + + self.n_trials = trials_to_use_congruent + trials_to_use_incongruent + + + selected_congruent = np.random.choice(self.congruent_stims, size=trials_to_use_congruent, replace=True).tolist() + selected_incongruent = np.random.choice(self.incongruent_stims, size=trials_to_use_incongruent, replace=True).tolist() + + trial_data = [] + for stim_text in selected_congruent: + trial_data.append({'stim_text': stim_text, 'type': 'congruent'}) + for stim_text in selected_incongruent: + trial_data.append({'stim_text': stim_text, 'type': 'incongruent'}) + + + shuffle(trial_data) + + + self.trials = pd.DataFrame(trial_data) + self.trials['iti'] = self.iti + np.random.rand(len(self.trials)) * self.jitter + self.trials['soa'] = self.soa + + + + self.text_stim = visual.TextStim(win=self.window, text="", color='white', height=0.1) + self.fixation = visual.GratingStim(win=self.window, size=0.2, pos=[0, 0], sf=0, rgb=[1, 0, 0]) + self.fixation.setAutoDraw(True) + + + self.window.flip() + + return None + + def present_stimulus(self, idx: int): + """ + Presents the current word/sentence stimulus and sends a marker to the EEG. + """ + + current_trial = self.trials.iloc[idx] + word_to_display = current_trial['stim_text'] + stim_type = current_trial['type'] + marker_value = self.markernames[stim_type] + + + self.text_stim.text = word_to_display + self.text_stim.draw() + self.fixation.draw() + + + self.window.flip() + + + if self.eeg: + timestamp = time() + + marker = [int(marker_value)] if self.eeg.backend == "muselsl" else int(marker_value) + self.eeg.push_sample(marker=marker, timestamp=timestamp) + + + core.wait(self.soa) + + + self.text_stim.text = '' + self.window.flip() + + + core.wait(current_trial['iti']) \ No newline at end of file diff --git a/eegnb/stimuli/__init__.py b/eegnb/stimuli/__init__.py index 551ad0a6..c1c36864 100644 --- a/eegnb/stimuli/__init__.py +++ b/eegnb/stimuli/__init__.py @@ -2,3 +2,4 @@ FACE_HOUSE = path.join(path.dirname(__file__), "visual", "face_house") CAT_DOG = path.join(path.dirname(__file__), "visual", "cats_dogs") +N400_TEXT = path.join(path.dirname(path.abspath(__file__)), "text") diff --git a/eegnb/stimuli/text/LICENSE.txt b/eegnb/stimuli/text/LICENSE.txt new file mode 100644 index 00000000..48c92339 --- /dev/null +++ b/eegnb/stimuli/text/LICENSE.txt @@ -0,0 +1,3 @@ +All the text data were taken from N400Stimset_stimuli_parameters.tsv from the paper given below +Evoking the N400 event-related potential (ERP) component using a publicly available novel set of sentences with semantically incongruent or congruent eggplants +they are Public Domain (CC0). \ No newline at end of file diff --git a/eegnb/stimuli/text/congruent/C_text1.txt b/eegnb/stimuli/text/congruent/C_text1.txt new file mode 100644 index 00000000..4587ffe6 --- /dev/null +++ b/eegnb/stimuli/text/congruent/C_text1.txt @@ -0,0 +1 @@ +The cake is in the oven to bake. diff --git a/eegnb/stimuli/text/congruent/C_text2.txt b/eegnb/stimuli/text/congruent/C_text2.txt new file mode 100644 index 00000000..ae4bb971 --- /dev/null +++ b/eegnb/stimuli/text/congruent/C_text2.txt @@ -0,0 +1 @@ +In the winter the pond will freeze. \ No newline at end of file diff --git a/eegnb/stimuli/text/congruent/C_text3.txt b/eegnb/stimuli/text/congruent/C_text3.txt new file mode 100644 index 00000000..ef6db151 --- /dev/null +++ b/eegnb/stimuli/text/congruent/C_text3.txt @@ -0,0 +1 @@ +The soccer team scored a goal. \ No newline at end of file diff --git a/eegnb/stimuli/text/congruent/C_text4.txt b/eegnb/stimuli/text/congruent/C_text4.txt new file mode 100644 index 00000000..4bddd571 --- /dev/null +++ b/eegnb/stimuli/text/congruent/C_text4.txt @@ -0,0 +1 @@ +My mom puts money in the bank. \ No newline at end of file diff --git a/eegnb/stimuli/text/congruent/C_text5.txt b/eegnb/stimuli/text/congruent/C_text5.txt new file mode 100644 index 00000000..a134a583 --- /dev/null +++ b/eegnb/stimuli/text/congruent/C_text5.txt @@ -0,0 +1 @@ +His mom makes her own pie crust. \ No newline at end of file diff --git a/eegnb/stimuli/text/incongruent/IC_text1.txt b/eegnb/stimuli/text/incongruent/IC_text1.txt new file mode 100644 index 00000000..41f37b67 --- /dev/null +++ b/eegnb/stimuli/text/incongruent/IC_text1.txt @@ -0,0 +1 @@ +He puts gas in his chest. diff --git a/eegnb/stimuli/text/incongruent/IC_text2.txt b/eegnb/stimuli/text/incongruent/IC_text2.txt new file mode 100644 index 00000000..961c2c56 --- /dev/null +++ b/eegnb/stimuli/text/incongruent/IC_text2.txt @@ -0,0 +1 @@ +He put on his shoes to go apple. \ No newline at end of file diff --git a/eegnb/stimuli/text/incongruent/IC_text3.txt b/eegnb/stimuli/text/incongruent/IC_text3.txt new file mode 100644 index 00000000..a6e046fc --- /dev/null +++ b/eegnb/stimuli/text/incongruent/IC_text3.txt @@ -0,0 +1 @@ +Babies sleep in shot. \ No newline at end of file diff --git a/eegnb/stimuli/text/incongruent/IC_text4.txt b/eegnb/stimuli/text/incongruent/IC_text4.txt new file mode 100644 index 00000000..e3ece84f --- /dev/null +++ b/eegnb/stimuli/text/incongruent/IC_text4.txt @@ -0,0 +1 @@ +The angry boys got in a mouse. \ No newline at end of file diff --git a/eegnb/stimuli/text/incongruent/IC_text5.txt b/eegnb/stimuli/text/incongruent/IC_text5.txt new file mode 100644 index 00000000..160ccfe5 --- /dev/null +++ b/eegnb/stimuli/text/incongruent/IC_text5.txt @@ -0,0 +1 @@ +The cake is in the oven to milk. \ No newline at end of file