KBB CPT Triggers (PsychoPy) - LeoLedesma237/LeoWebsite GitHub Wiki
Below is the code for the triggers in the CPT task. Triggers are signals sent by software -in our case PsychoPy- that give information of the stimulus type and its latency. This is crucial for EEG research that incorporates event related potentials analysis. PsychoPy has code templates available for trigger sending via this link: Sending triggers via serial port. This will function for devices that have a serial port (like we do).
Our CPT is comprised of two blocks/routines in this order:
- CPT Practice
- CPT Real Task
We start first by identifying which port name needs to be added into the code. Open 'Device Manger' and scroll to Ports (COM & LPT). Once there, remove and plug in the TriggerBox from your computer's port and notice which port name disappears/reappears from this window. That will be the name of the port to write into the code. Ours is COM4.
The next steps will almost be identical to those in the link mentioned above. Sending triggers via serial port
There is a clear distinction in this task than is present in the KBB MMN Triggers (PsychoPy) page. The biggest difference comes from this task not being passive- thus we need to also keep track of subject responses throughout the task.
When working with software such as BrainVision Recorder, there is an option to check the Digital Port Settings of the amplifier. Doing this will bring up a table that contain eight bits (numbers 0 to 7) that have a corresponding trigger type (Stimulus or Response). This is very important because it limits what numeric values you can utilize in your code to represent either of these trigger types. To make sense of this we first need to learn a little bit of binary.
Binary numbers are another way to transfer numeric information by a string of 0's and 1's. Therefore, any number you can think of has a binary number counterpart. To understand how this works, we can create a table below where the first two rows of each column describe the name of the Bit and a corresponding numeric value associated with it. The Bit 0 is associated with the value 1. Bit 1 is equal to the value of Bit 0 x 2 (1 x 2 = 2). Bit 2 is equal to the value of Bit 1 x 2 (2 x 2 = 4), and this same pattern continues until Bit 7. Therefore, each Bit #'s value is the value of the previous Bit x 2.
To create a binary number, first we need to think of a regular numeric number, such as 6, and then identify which combination of 0's and 1's for each Bit value will create a sum of 6. These 0's and 1's function as coefficients to these Bit values. Thus, if we wanted the number 4 in binary, then we would give zeroes to Bit 7, 6, 5, 4, 3, 1, and 0, and give Bit 2 the value of 1 because 1 x 4 = 1. The same logic would apply to get the number 2, we would give zeroes to Bit 7, 6, 5, 4, 3, 2, and 0, and give Bit 1 the value of 1 because 1 x 2 = 2. Now then, to get the number 6, we would just give Bit 2 and Bit 1 the values of 1 (every other Bit gets zero), which returns the values 4 and 2. These values get added up to make 6. Thus, the binary number of 6 is 00000110 or we can also write 110. Below is a table showing more examples of numbers (green) and how they are expressed in binary.
![](https://private-user-images.githubusercontent.com/81386436/333841155-b3d73d5e-4b19-42b4-b559-2db7aad40e33.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjUxNDk5MDUsIm5iZiI6MTcyNTE0OTYwNSwicGF0aCI6Ii84MTM4NjQzNi8zMzM4NDExNTUtYjNkNzNkNWUtNGIxOS00MmI0LWI1NTktMmRiN2FhZDQwZTMzLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA5MDElMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwOTAxVDAwMTMyNVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTY3YzU3MjFiYjZlODUwZDc1ZDQ0ZTYxNjViMjY0ZGQzZjBiZDZhYmE4N2JhYmE1ODQ4MGRjMjE0Mzc4NGU0ZWMmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.TGz_6vuQG47mfSCRjICZRuNvtj3rijxQx35CiwExFR8)
Now we can take this a step further and merge the concepts of trigger bits, binary numbers, and bit types. BrainVision has a pretty complex way of labeling a trigger as a stimulus or as a response. For example, based on the set up we have in the image in 'Trigger Bit', we have allowed Bits 0, 1, 2, and 3, (so four bits) to be coded as Stimulus. Additionally, we have allowed Bits 4, 5, 6, 7, (also four bits) to be coded as Response. Therefore, for to correctly program our stimulus triggers in PsychoPy, we need to code in numbers that their binary version have at least a value of 1 in either Stimulus coded Bits or Response coded Bits but not both. Going back the binary number table from above, the numbers 1 through 15 will function as good stimulus trigger codes because their binary number counterpart only have the number 1 in Bits 0, 1, 2, and/or 3 only. The number 37, for example would not be a good stimulus trigger code because it has 1's in Bits that are coded for Stimulus (Bits 0 and 2) and for Responses (Bit 5). Additionally, the number 37 would also be a bad response trigger code for the same reason. A good response trigger code number would be 32 since it has a value of 1 in Bit 5 and no 1's in any of the Bits that are coded to be Stimuli. Below is a table showing trigger code numbers that are good candidates for either stimulus or responses based on the digital port settings we have.
![](https://private-user-images.githubusercontent.com/81386436/333841805-25f21b72-71aa-4a83-ac45-6aecbe3d07cf.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjUxNDk5MDUsIm5iZiI6MTcyNTE0OTYwNSwicGF0aCI6Ii84MTM4NjQzNi8zMzM4NDE4MDUtMjVmMjFiNzItNzFhYS00YTgzLWFjNDUtNmFlY2JlM2QwN2NmLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA5MDElMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwOTAxVDAwMTMyNVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTk5OTgzODUzMmMyMjNhYmE3Zjg4YTZhODhjNjg0NDM1ZDAxOTUzNzkwYTVlZWFlNzljNjhmMmEyMzJhZmUxMDMmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.liXis94CmEfz6S6SDe0GW9qR6X9ianLai-9WVJNvz08)
Tab: Begin Experiment
# Import the necessary libraries
import serial
# Open a connection to the serial port (replace 'COM4' with your actual port)
ser = serial.Serial('COM4', baudrate=115200)
Tab: Begin Routine
stimulus_pulse_started = False
stimulus_pulse_ended = False
response_pulse_started = False
response_pulse_ended = False
Tab: Each Frame
This section of the code relates to the triggers being sent. The first section is specifically towards the stimulus. There are six different stimuli trigger codes. Those that are even correspond to target trials and those that are odd correspond to even trials. The number size for even and odd triggers relate to the ISI of the trial.
The response triggers make up the bottom portion of the code. If the spacebar is pressed during the trial, then the trigger value [0x80] = 128 is sent. If the spacebar is not pressed during the trial, then the trigger value [0xA0] = 160 is sent. These values correspond with appropriate trigger bits, thus they should appear as response stimulus in the EEG.
if image_trial.status == STARTED and not stimulus_pulse_started:
# Check if the current stimulus is the deviant or standard
if Trial_Type_Spec == 'Target_1':
trigger_code = '2'
elif Trial_Type_Spec == 'Target_2':
trigger_code = '4'
elif Trial_Type_Spec == 'Target_4':
trigger_code = '6'
elif Trial_Type_Spec == 'Non-target_1':
trigger_code = '1'
elif Trial_Type_Spec == 'Non-target_2':
trigger_code = '3'
else:
trigger_code = '5'
# Send the trigger signal
win.callOnFlip(ser.write, str.encode(trigger_code))
# Record the start time of the trigger pulse
stimulus_pulse_start_time = globalClock.getTime()
# Mark that the trigger pulse has started
stimulus_pulse_started = True
if stimulus_pulse_started and not stimulus_pulse_ended:
if globalClock.getTime() - stimulus_pulse_start_time >= 5:
# Send the trigger signal to end the pulse
win.callOnFlip(ser.write, str.encode('0'))
# Mark that the trigger pulse has ended
stimulus_pulse_ended = True
# code to send the trigger for a response
if image_trial_response.keys == 'space' and not response_pulse_started:
# Mark that the trigger pulse has started
response_pulse_started = True
# Send the response code
ser.write([0x80])
if CPT_ISI.status == FINISHED and not response_pulse_started:
# Mark that the trigger pulse has started
response_pulse_started = True
# Send the response code
ser.write([0xA0])
Tab: End Experiment
port.close()
Below we can see that there are blue boxes with R's. These indicate that response triggers, either by the pressing of the 'spacebar' or not are both being read by the EEG system. The red boxes indicate stimuli that are presented. Because the stimulus and response triggers overlap, it is difficult to understand exactly which one is being shown, however, their presence is a quick way to test that the signal is being by the EEG (sometimes at least).
The next and more thorough approach is to record EEG data and view its files content. When recording brain data from BrainVision Recorder, three separate files are created. They are the following:
- .eeg
- .vhdr
- .vmrk
We are most interested in the .vmrk file, which specifically records the types of triggers, their order of appearance, and their latency. Below is an image of the layout of the .vmrk file. We can see that there are triggers associated with the stimulus (S) and with the response (R). The S 4 indicates a target trial with an IS of two seconds, the R 12 represents the subject pressed the spacebar. R 4 represents when the spacebar can be pressed and R 7 represents when the spacebar can no longer be pressed (because that trial has ended). The response trigger R 14 appears when the spacebar was not pressed for that trial. These responses do not code for whether the trial was correct or not- but that can be determined in later stages of preprocessing.
We can inspect the .vmrk file further by using R. However, for the file to load in correctly, we must skip the first 12 rows. We can do this by adding it as an argument into the read.csv() function. The file inspection below gives us much more needed information about the task to ensure it is doing what initially was programmed.
The R script below is doing the following. It only looks at the stimuli and response trigger codes of interest. It then calculates the latency of the trial in milliseconds and obtains the subject's reaction time for each trial. Additionally, it reports the number of trials, the counts for each type of stimulus presented, and the latency in milliseconds between trials. The outputs show that everything is working as intended.
This frequency of stimulus and response triggers add up to 360 respectively.
Trigger | Frequency |
---|---|
S 1 | 12 |
S 2 | 108 |
S 3 | 12 |
S 4 | 108 |
S 5 | 12 |
S 6 | 108 |
R 12 | 334 |
R 14 | 26 |
The median latency in ms between trials is below. This is as expected since they represent the coded ISI (1, 2, or 4 seconds) plus the duration of the stimulus (250 ms).
Trigger | Avg Latency ms |
---|---|
S 1 | 1250 |
S 2 | 1250 |
S 3 | 2250 |
S 4 | 2250 |
S 5 | 4250 |
S 6 | 4250 |
This R script should work on any .vmrk file specifically for the CPT. It will only work as intended if the full EEG recording session was completed.
# Load in packages
library(tidyverse)
library(psych)
# Set the working directory
setwd("C:/Users/uhonr/Desktop/EEG Testing")
# Load in the data for the markers
data <- read.csv("CPT - Copy.vmrk", header = F, skip = 12)
# Get to know our data
dim(data)
# Rename the dataset
names(data) <- c("Response", "Response.Abv", "Latency", "One", "Zero")
# Remove unnecessary triggers
data <- data %>%
filter(!(Response.Abv %in% c("R 4", "R 7")))
# Convert Latency into milliseconds by multipying it by 1000 divided by the sampling rate
sampling.rate = 500
data$Latency.ms <- data$Latency * 1000/sampling.rate
# Keep the Response.Abv and the Latency.ms Column
data <- data %>%
select(Response.Abv,Latency.ms)
# Pair each stimulus and response trigger
Stimulus.data <- data[seq(from = 1, to = nrow(data), by = 2),]
Response.data <- data[seq(from = 2, to = nrow(data), by = 2),]
Paired.data <- cbind(Stimulus.data, Response.data)
# Rename the dataset again
names(Paired.data) <- c("Stimulus", "S.Latency.ms", "Response", "R.Latency.ms")
# Quality check
unique(Paired.data$Stimulus)
unique(Paired.data$Response)
# Take the difference in latency to know trial length
Paired.data$ReactionTime.ms <- Paired.data$R.Latency.ms - Paired.data$S.Latency.ms
# Create a variable that records time between trials
Paired.data$Latency.ms.Between.Trials <- c(NA ,Paired.data$S.Latency.ms[2:nrow(Paired.data)] - Paired.data$S.Latency.ms[1:nrow(Paired.data)-1])
# Quality control of Latency between trials by stimulus type
Paired.data %>%
group_by(Stimulus) %>%
summarise(Avg.Latency.ms = median(Latency.ms.Between.Trials, na.rm = TRUE))
# Use the describe function to get more information
describeBy(Paired.data$Latency.ms.Between.Trials, Paired.data$Stimulus)
# Obtain the count of stimuli
table(Paired.data$Stimulus)
# Obtain the count of responses
table(Paired.data$Response)