From d2ce53d050f614adb06dcd6cead69c26fe96703e Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Sun, 27 Aug 2017 00:11:25 -0700 Subject: [PATCH 01/30] Version bump --- interface/about_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/about_dialog.py b/interface/about_dialog.py index b0c9a86..0fd1704 100644 --- a/interface/about_dialog.py +++ b/interface/about_dialog.py @@ -12,7 +12,7 @@ def __init__(self, parent=None): self.setupUi(self) # Set version number - self.versionValue.setText("3.1.0") + self.versionValue.setText("3.2.0") # Add icon image base_dir = os.path.dirname(os.path.dirname( From d2c6eb47a49fdf333f1c3554fa42ae4dea2c364a Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Sun, 27 Aug 2017 00:17:06 -0700 Subject: [PATCH 02/30] Add placeholder text in changelog and readme --- CHANGELOG.md | 5 +++++ README.md | 2 ++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 324ba54..5b6039d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## [3.2.0](https://github.com/sho-87/cognitive-battery/releases/tag/3.2.0) *(2017-08-xx)* + +**Tasks** +- Added Eriksen Flanker task + ## [3.1.0](https://github.com/sho-87/cognitive-battery/releases/tag/3.1.0) *(2017-07-06)* **General** diff --git a/README.md b/README.md index 26a5faa..e13d1e4 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ Currently implemented tasks: - Based on the version used by [Berman et al. (2008)](http://pss.sagepub.com/content/19/12/1207) and [Bourrier (2015)](https://open.library.ubc.ca/cIRcle/collections/ubctheses/24/items/1.0166677) +* **Eriksen Flanker Task** + - Based on the version by xxx * **Mental Rotation Task** - Based on the redrawn version by [Peters et al. (1995)](http://www.ncbi.nlm.nih.gov/pubmed/7546667) From 1b79db93e1d212515a9c37ec1e687235efdde51b Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Sun, 27 Aug 2017 00:41:46 -0700 Subject: [PATCH 03/30] Add flanker battery options --- designer/battery_window_qt.py | 11 ++++++-- designer/settings_window_qt.py | 22 +++++++++++---- designer/ui/battery_window_qt.ui | 8 ++++++ designer/ui/settings_window_qt.ui | 47 +++++++++++++++++++++++++++---- 4 files changed, 75 insertions(+), 13 deletions(-) diff --git a/designer/battery_window_qt.py b/designer/battery_window_qt.py index aefff4b..240962e 100644 --- a/designer/battery_window_qt.py +++ b/designer/battery_window_qt.py @@ -168,6 +168,9 @@ def setupUi(self, CognitiveBattery): item = QtWidgets.QListWidgetItem() item.setCheckState(QtCore.Qt.Unchecked) self.taskList.addItem(item) + item = QtWidgets.QListWidgetItem() + item.setCheckState(QtCore.Qt.Unchecked) + self.taskList.addItem(item) self.taskListLayout.addWidget(self.taskList) self.reorderButtonLayout = QtWidgets.QVBoxLayout() self.reorderButtonLayout.setContentsMargins(0, -1, -1, -1) @@ -294,12 +297,14 @@ def retranslateUi(self, CognitiveBattery): item = self.taskList.item(1) item.setText(_translate("CognitiveBattery", "Digit Span (backwards)")) item = self.taskList.item(2) - item.setText(_translate("CognitiveBattery", "Mental Rotation Task")) + item.setText(_translate("CognitiveBattery", "Eriksen Flanker Task")) item = self.taskList.item(3) - item.setText(_translate("CognitiveBattery", "Raven\'s Progressive Matrices")) + item.setText(_translate("CognitiveBattery", "Mental Rotation Task")) item = self.taskList.item(4) - item.setText(_translate("CognitiveBattery", "Sternberg Task")) + item.setText(_translate("CognitiveBattery", "Raven\'s Progressive Matrices")) item = self.taskList.item(5) + item.setText(_translate("CognitiveBattery", "Sternberg Task")) + item = self.taskList.item(6) item.setText(_translate("CognitiveBattery", "Sustained Attention to Response Task (SART)")) self.taskList.setSortingEnabled(__sortingEnabled) self.upButton.setStatusTip(_translate("CognitiveBattery", "Move selected task up in order of administration")) diff --git a/designer/settings_window_qt.py b/designer/settings_window_qt.py index 43537dc..8db0626 100644 --- a/designer/settings_window_qt.py +++ b/designer/settings_window_qt.py @@ -12,7 +12,7 @@ class Ui_SettingsDialog(object): def setupUi(self, SettingsDialog): SettingsDialog.setObjectName("SettingsDialog") SettingsDialog.setWindowModality(QtCore.Qt.ApplicationModal) - SettingsDialog.resize(415, 296) + SettingsDialog.resize(415, 320) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -29,7 +29,7 @@ def setupUi(self, SettingsDialog): self.scrollArea.setWidgetResizable(True) self.scrollArea.setObjectName("scrollArea") self.scrollAreaWidgetContents = QtWidgets.QWidget() - self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 391, 241)) + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 391, 265)) self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents) self.verticalLayout_5.setContentsMargins(0, 0, 0, 0) @@ -42,7 +42,7 @@ def setupUi(self, SettingsDialog): self.settings_toolbox.setFrameShape(QtWidgets.QFrame.NoFrame) self.settings_toolbox.setObjectName("settings_toolbox") self.general_page = QtWidgets.QWidget() - self.general_page.setGeometry(QtCore.QRect(0, 0, 373, 115)) + self.general_page.setGeometry(QtCore.QRect(0, 0, 373, 112)) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -90,7 +90,7 @@ def setupUi(self, SettingsDialog): self.verticalLayout_7.addItem(spacerItem) self.settings_toolbox.addItem(self.general_page, "") self.ant_page = QtWidgets.QWidget() - self.ant_page.setGeometry(QtCore.QRect(0, 0, 373, 115)) + self.ant_page.setGeometry(QtCore.QRect(0, 0, 373, 112)) self.ant_page.setObjectName("ant_page") self.horizontalLayout = QtWidgets.QHBoxLayout(self.ant_page) self.horizontalLayout.setContentsMargins(0, 0, 0, 0) @@ -106,7 +106,18 @@ def setupUi(self, SettingsDialog): self.ant_form.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.settings_ant_blocks_value) self.horizontalLayout.addLayout(self.ant_form) self.settings_toolbox.addItem(self.ant_page, "") + self.flanker_page = QtWidgets.QWidget() + self.flanker_page.setObjectName("flanker_page") + self.verticalLayout = QtWidgets.QVBoxLayout(self.flanker_page) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.flanker_form = QtWidgets.QFormLayout() + self.flanker_form.setContentsMargins(10, -1, 10, -1) + self.flanker_form.setObjectName("flanker_form") + self.verticalLayout.addLayout(self.flanker_form) + self.settings_toolbox.addItem(self.flanker_page, "") self.ravens_page = QtWidgets.QWidget() + self.ravens_page.setGeometry(QtCore.QRect(0, 0, 373, 112)) self.ravens_page.setObjectName("ravens_page") self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.ravens_page) self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) @@ -129,7 +140,7 @@ def setupUi(self, SettingsDialog): self.verticalLayout_4.addLayout(self.formLayout) self.settings_toolbox.addItem(self.ravens_page, "") self.sternberg_page = QtWidgets.QWidget() - self.sternberg_page.setGeometry(QtCore.QRect(0, 0, 373, 115)) + self.sternberg_page.setGeometry(QtCore.QRect(0, 0, 373, 112)) self.sternberg_page.setObjectName("sternberg_page") self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.sternberg_page) self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) @@ -178,6 +189,7 @@ def retranslateUi(self, SettingsDialog): self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.general_page), _translate("SettingsDialog", "General")) self.settings_ant_blocks_label.setText(_translate("SettingsDialog", "Number of blocks:")) self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.ant_page), _translate("SettingsDialog", "Attention Network Test")) + self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.flanker_page), _translate("SettingsDialog", "Eriksen Flanker Task")) self.settings_ravens_start_label.setText(_translate("SettingsDialog", "Starting image #:")) self.settings_ravens_trials_label.setText(_translate("SettingsDialog", "Number of trials:")) self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.ravens_page), _translate("SettingsDialog", "Raven\'s Progressive Matrices")) diff --git a/designer/ui/battery_window_qt.ui b/designer/ui/battery_window_qt.ui index 359b941..18dbe89 100644 --- a/designer/ui/battery_window_qt.ui +++ b/designer/ui/battery_window_qt.ui @@ -310,6 +310,14 @@ Unchecked + + + Eriksen Flanker Task + + + Unchecked + + Mental Rotation Task diff --git a/designer/ui/settings_window_qt.ui b/designer/ui/settings_window_qt.ui index 986098b..b8462cd 100644 --- a/designer/ui/settings_window_qt.ui +++ b/designer/ui/settings_window_qt.ui @@ -10,7 +10,7 @@ 0 0 415 - 296 + 320 @@ -45,7 +45,7 @@ 0 0 391 - 241 + 265 @@ -69,7 +69,7 @@ 0 0 373 - 115 + 112 @@ -190,7 +190,7 @@ 0 0 373 - 115 + 112 @@ -231,7 +231,44 @@ + + + Eriksen Flanker Task + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 10 + + + 10 + + + + + + + + 0 + 0 + 373 + 112 + + Raven's Progressive Matrices @@ -286,7 +323,7 @@ 0 0 373 - 115 + 112 From e5de5f097f65ab074d06d8e4b7bc198b60405cd2 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Sun, 27 Aug 2017 00:54:34 -0700 Subject: [PATCH 04/30] Run flanker from battery window --- interface/battery_window.py | 44 +++-- tasks/README.md | 2 + tasks/flanker.py | 358 ++++++++++++++++++++++++++++++++++++ 3 files changed, 385 insertions(+), 19 deletions(-) create mode 100644 tasks/flanker.py diff --git a/interface/battery_window.py b/interface/battery_window.py index a8d7700..7ab34e7 100644 --- a/interface/battery_window.py +++ b/interface/battery_window.py @@ -9,7 +9,7 @@ from utils import display from designer import battery_window_qt from interface import about_dialog, settings_window -from tasks import ant, mrt, sart, ravens, digitspan_backwards, sternberg +from tasks import ant, flanker, mrt, sart, ravens, digitspan_backwards, sternberg class BatteryWindow(QtWidgets.QMainWindow, battery_window_qt.Ui_CognitiveBattery): @@ -263,7 +263,7 @@ def get_settings(self): # Override the closeEvent method def closeEvent(self, event): self.save_main_window_settings(self.size(), self.pos()) - + event.accept() sys.exit(0) # This closes any open pygame windows @@ -347,10 +347,10 @@ def start(self): pygame.init() # Load beep sound - beep_sound = pygame.mixer.Sound(os.path.join(self.base_dir, - 'tasks', - 'media', - 'beep_med.wav')) + beep_sound = pygame.mixer.Sound(os.path.join(self.base_dir, + 'tasks', + 'media', + 'beep_med.wav')) # Set pygame icon image image = os.path.join(self.base_dir, "images", "icon_sml.png") @@ -385,18 +385,6 @@ def start(self): ant_data = ant_task.run() # Save ANT data to excel ant_data.to_excel(writer, 'ANT', index=False) - elif task == "Mental Rotation Task": - mrt_task = mrt.MRT(self.pygame_screen, background) - # Run MRT - mrt_data = mrt_task.run() - # Save MRT data to excel - mrt_data.to_excel(writer, 'MRT', index=False) - elif task == "Sustained Attention to Response Task (SART)": - sart_task = sart.SART(self.pygame_screen, background) - # Run SART - sart_data = sart_task.run() - # Save SART data to excel - sart_data.to_excel(writer, 'SART', index=False) elif task == "Digit Span (backwards)": digitspan_backwards_task = \ digitspan_backwards.DigitspanBackwards( @@ -407,6 +395,18 @@ def start(self): # Save digit span (backwards) data to excel digitspan_backwards_data.to_excel( writer, 'Digit span (backwards)', index=False) + elif task == "Eriksen Flanker Task": + flanker_task = flanker.Flanker(self.pygame_screen, background) + # Run Eriksen Flanker + flanker_data = flanker_task.run() + # Save flanker data to excel + flanker_data.to_excel(writer, 'Eriksen Flanker', index=False) + elif task == "Mental Rotation Task": + mrt_task = mrt.MRT(self.pygame_screen, background) + # Run MRT + mrt_data = mrt_task.run() + # Save MRT data to excel + mrt_data.to_excel(writer, 'MRT', index=False) elif task == "Raven's Progressive Matrices": ravens_task = ravens.Ravens( self.pygame_screen, background, @@ -425,7 +425,13 @@ def start(self): # Save sternberg data to excel sternberg_data.to_excel(writer, 'Sternberg', index=False) - + elif task == "Sustained Attention to Response Task (SART)": + sart_task = sart.SART(self.pygame_screen, background) + # Run SART + sart_data = sart_task.run() + # Save SART data to excel + sart_data.to_excel(writer, 'SART', index=False) + # Play beep after each task if self.task_beep: beep_sound.play() diff --git a/tasks/README.md b/tasks/README.md index 3d919bc..d9e0d9a 100644 --- a/tasks/README.md +++ b/tasks/README.md @@ -23,6 +23,8 @@ - Digit duration: 1000ms - Time between digits: 100ms +## Eriksen Flanker Task + ## Mental Rotation Task (MRT) - 3 practice questions - 2 main blocks (with a break inbetween) diff --git a/tasks/flanker.py b/tasks/flanker.py new file mode 100644 index 0000000..55cb61f --- /dev/null +++ b/tasks/flanker.py @@ -0,0 +1,358 @@ +import os +import sys +import time +import pandas as pd +import numpy as np +import pygame + +from pygame.locals import * +from itertools import product +from utils import display + + +class Flanker(object): + def __init__(self, screen, background, blocks=3): + # Get the pygame display window + self.screen = screen + self.background = background + + # Sets font and font size + self.font = pygame.font.SysFont("arial", 30) + + # Get screen info + self.screen_x = self.screen.get_width() + self.screen_y = self.screen.get_height() + + # Fill background + self.background.fill((255, 255, 255)) + pygame.display.set_caption("Eriksen Flanker Task") + pygame.mouse.set_visible(0) + + # Experiment options + self.NUM_BLOCKS = blocks + self.FIXATION_DURATION_RANGE = (400, 1600) # Range of fixation times + self.CUE_DURATION = 100 + self.PRE_STIM_FIXATION_DURATION = 400 + self.TARGET_OFFSET = 31 # Stimulus vertical offset + self.FLANKER_DURATION = 1700 + self.FEEDBACK_DURATION = 1000 + self.ITI_MAX = 3500 + + # Specify factor levels, and task timings as used by Fan et al. (2002). + self.CONGRUENCY_LEVELS = ("congruent", "incongruent", 'neutral') + self.CUE_LEVELS = ("nocue", "center", "spatial", 'double') + self.LOCATION_LEVELS = ('top', 'bottom') + self.DIRECTION_LEVELS = ('left', 'right') + + # Create level combinations + # Level combinations give us 48 trials. + self.combinations = list( + product(self.CONGRUENCY_LEVELS, self.CUE_LEVELS, + self.LOCATION_LEVELS, self.DIRECTION_LEVELS)) + + # Get images + self.base_dir = os.path.dirname(os.path.realpath(__file__)) + self.image_path = os.path.join(self.base_dir, "images", "ANT") + + self.img_left_congruent = pygame.image.load( + os.path.join(self.image_path, 'left_congruent.png')) + self.img_left_incongruent = pygame.image.load( + os.path.join(self.image_path, 'left_incongruent.png')) + self.img_right_congruent = pygame.image.load( + os.path.join(self.image_path, 'right_congruent.png')) + self.img_right_incongruent = pygame.image.load( + os.path.join(self.image_path, 'right_incongruent.png')) + self.img_left_neutral = pygame.image.load( + os.path.join(self.image_path, 'left_neutral.png')) + self.img_right_neutral = pygame.image.load( + os.path.join(self.image_path, 'right_neutral.png')) + + self.img_fixation = pygame.image.load( + os.path.join(self.image_path, 'fixation.png')) + self.img_cue = pygame.image.load( + os.path.join(self.image_path, 'cue.png')) + + # Get image dimensions + self.flanker_h = self.img_left_incongruent.get_rect().height + self.fixation_h = self.img_fixation.get_rect().height + + # Create output dataframe + self.all_data = pd.DataFrame() + + def create_block(self, block_num, combinations, trial_type): + if trial_type == "main": + cur_combinations = combinations * 2 + np.random.shuffle(cur_combinations) + else: + np.random.shuffle(combinations) + cur_combinations = combinations[:len(combinations)//2] + + # Add combinations to dataframe + cur_block = pd.DataFrame(data=cur_combinations, columns=( + 'congruency', 'cue', 'location', 'direction')) + + # Add timing info to dataframe + cur_block["block"] = block_num + 1 + cur_block["fixationTime"] = [x for x in np.random.randint( + self.FIXATION_DURATION_RANGE[0], self.FIXATION_DURATION_RANGE[1], + len(cur_combinations))] + + return cur_block + + def display_flanker(self, flanker_type, location, direction): + # Left flanker + if direction == "left": + if flanker_type == "congruent": + stimulus = self.img_left_congruent + elif flanker_type == "incongruent": + stimulus = self.img_left_incongruent + else: + stimulus = self.img_left_neutral + # Right flanker + else: + if flanker_type == "congruent": + stimulus = self.img_right_congruent + elif flanker_type == "incongruent": + stimulus = self.img_right_incongruent + else: + stimulus = self.img_right_neutral + + # Offset the flanker stimulus to above/below fixation + if location == "top": + display.image( + self.screen, stimulus, "center", + self.screen_y/2 - self.flanker_h - self.TARGET_OFFSET) + elif location == "bottom": + display.image(self.screen, stimulus, "center", + self.screen_y/2 + self.TARGET_OFFSET) + + def display_trial(self, trial_num, data, trial_type): + # Check for a quit press after stimulus was shown + for event in pygame.event.get(): + if event.type == KEYDOWN and event.key == K_F12: + sys.exit(0) + + # Display fixation + self.screen.blit(self.background, (0, 0)) + display.image(self.screen, self.img_fixation, "center", "center") + pygame.display.flip() + + display.wait(data["fixationTime"][trial_num]) + + # Display cue + self.screen.blit(self.background, (0, 0)) + + cue_type = data["cue"][trial_num] + + if cue_type == "nocue": + # Display fixation in the center + display.image(self.screen, self.img_fixation, "center", "center") + elif cue_type == "center": + # Display cue in the center + display.image(self.screen, self.img_cue, "center", "center") + elif cue_type == "double": + # Display fixation in the center + display.image(self.screen, self.img_fixation, "center", "center") + + # Display cue above and below fixation + display.image( + self.screen, self.img_cue, "center", + self.screen_y/2 - self.fixation_h - self.TARGET_OFFSET) + display.image(self.screen, self.img_cue, + "center", self.screen_y/2 + self.TARGET_OFFSET) + elif cue_type == "spatial": + cue_location = data["location"][trial_num] + + # Display fixation in the center + display.image(self.screen, self.img_fixation, "center", "center") + + # Display cue at target location + if cue_location == "top": + display.image( + self.screen, self.img_cue, "center", + self.screen_y/2 - self.fixation_h - self.TARGET_OFFSET) + elif cue_location == "bottom": + display.image(self.screen, self.img_cue, "center", + self.screen_y/2 + self.TARGET_OFFSET) + + pygame.display.flip() + + # Display cue for certain duration + display.wait(self.CUE_DURATION) + + # Prestim interval with fixation + self.screen.blit(self.background, (0, 0)) + display.image(self.screen, self.img_fixation, "center", "center") + pygame.display.flip() + + display.wait(self.PRE_STIM_FIXATION_DURATION) + + # Display flanker target + self.screen.blit(self.background, (0, 0)) + display.image(self.screen, self.img_fixation, "center", "center") + + self.display_flanker(data["congruency"][trial_num], + data["location"][trial_num], + data["direction"][trial_num]) + pygame.display.flip() + + start_time = int(round(time.time() * 1000)) + + # Clear the event queue before checking for responses + pygame.event.clear() + response = "NA" + wait_response = True + while wait_response: + for event in pygame.event.get(): + if event.type == KEYDOWN and event.key == K_LEFT: + response = "left" + wait_response = False + elif event.type == KEYDOWN and event.key == K_RIGHT: + response = "right" + wait_response = False + elif event.type == KEYDOWN and event.key == K_F12: + sys.exit(0) + + end_time = int(round(time.time() * 1000)) + + # If time limit has been reached, consider it a missed trial + if end_time - start_time >= self.FLANKER_DURATION: + wait_response = False + + # Store reaction time and response + rt = int(round(time.time() * 1000)) - start_time + data.set_value(trial_num, 'RT', rt) + data.set_value(trial_num, 'response', response) + + correct = 1 if response == data["direction"][trial_num] else 0 + data.set_value(trial_num, 'correct', correct) + + # Display feedback if practice trials + if trial_type == "practice": + self.screen.blit(self.background, (0, 0)) + if correct == 1: + display.text(self.screen, self.font, "correct", + "center", "center", (0, 255, 0)) + else: + display.text(self.screen, self.font, "incorrect", + "center", "center", (255, 0, 0)) + pygame.display.flip() + + display.wait(self.FEEDBACK_DURATION) + + # Display fixation during ITI + self.screen.blit(self.background, (0, 0)) + display.image(self.screen, self.img_fixation, "center", "center") + pygame.display.flip() + + iti = self.ITI_MAX - rt - data["fixationTime"][trial_num] + data.set_value(trial_num, 'ITI', iti) + + display.wait(iti) + + def run_block(self, block_num, total_blocks, block_type): + cur_block = self.create_block( + block_num, self.combinations, block_type) + + for i in range(cur_block.shape[0]): + self.display_trial(i, cur_block, block_type) + + if block_type == "main": + # Add block data to all_data + self.all_data = pd.concat([self.all_data, cur_block]) + + # End of block screen + if block_num != total_blocks - 1: # If not the final block + self.screen.blit(self.background, (0, 0)) + display.text(self.screen, self.font, + "End of current block. " + "Start next block when you're ready...", + 100, "center") + display.text_space(self.screen, self.font, + "center", (self.screen_y/2) + 100) + pygame.display.flip() + + display.wait_for_space() + + def run(self): + # Instructions + self.screen.blit(self.background, (0, 0)) + display.text(self.screen, self.font, "Eriksen Flanker Task", + "center", self.screen_y/2 - 300) + display.text(self.screen, self.font, + "Keep your eyes on the fixation cross at the " + "start of each trial:", + 100, self.screen_y/2 - 200) + display.image(self.screen, self.img_fixation, + "center", self.screen_y/2 - 150) + display.text(self.screen, self.font, + "A set of arrows will appear somewhere on the screen:", + 100, self.screen_y/2 - 100) + display.image(self.screen, self.img_left_incongruent, + "center", self.screen_y/2 - 50) + display.text(self.screen, self.font, + "Use the Left / Right arrow keys to indicate " + "the direction of the CENTER arrow.", + 100, self.screen_y/2 + 50) + display.text(self.screen, self.font, + "In example above, you should press the Left arrow.", + 100, self.screen_y/2 + 100) + display.text_space(self.screen, self.font, + "center", (self.screen_y/2) + 300) + pygame.display.flip() + + display.wait_for_space() + + # Instructions Practice + self.screen.blit(self.background, (0, 0)) + display.text(self.screen, self.font, + "We'll begin with a some practice trials...", + "center", "center") + display.text_space(self.screen, self.font, + "center", self.screen_y/2 + 100) + pygame.display.flip() + + display.wait_for_space() + + # Practice trials + self.run_block(0, 1, "practice") + + # Instructions Practice End + self.screen.blit(self.background, (0, 0)) + display.text(self.screen, self.font, + "We will now begin the main trials...", + 100, self.screen_y/2 - 50) + display.text(self.screen, self.font, + "You will not receive feedback after each trial.", + 100, self.screen_y/2 + 50) + display.text_space(self.screen, self.font, + "center", self.screen_y/2 + 200) + pygame.display.flip() + + display.wait_for_space() + + # Main task + for i in range(self.NUM_BLOCKS): + self.run_block(i, self.NUM_BLOCKS, "main") + + # Create trial number column + self.all_data["trial"] = list(range(1, len(self.all_data) + 1)) + + # Rearrange the dataframe + columns = ['trial', 'block', 'congruency', 'cue', 'location', + 'fixationTime', 'ITI', 'direction', + 'response', 'correct', 'RT'] + self.all_data = self.all_data[columns] + + # End screen + self.screen.blit(self.background, (0, 0)) + display.text(self.screen, self.font, "End of task", "center", "center") + display.text_space(self.screen, self.font, + "center", self.screen_y/2 + 100) + pygame.display.flip() + + display.wait_for_space() + + print("- Flanker complete") + + return self.all_data From dd518edd7139ee79a199f4fb9cd4b09a038c00e6 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Wed, 30 Aug 2017 23:03:57 -0700 Subject: [PATCH 05/30] Add references for Eriksen flanker task --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e13d1e4..c6ce1e4 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,8 @@ Currently implemented tasks: [Berman et al. (2008)](http://pss.sagepub.com/content/19/12/1207) and [Bourrier (2015)](https://open.library.ubc.ca/cIRcle/collections/ubctheses/24/items/1.0166677) * **Eriksen Flanker Task** - - Based on the version by xxx + - Based on the compatible and incompatible versions by [Hillman et al. (2006)](http://psycnet.apa.org/record/2006-20659-003) and + [Pontifex et al. (2011)](https://www.ncbi.nlm.nih.gov/pubmed/20521857) * **Mental Rotation Task** - Based on the redrawn version by [Peters et al. (1995)](http://www.ncbi.nlm.nih.gov/pubmed/7546667) From a584e1dabbab7399a2e015af7e1576cfc0b52a77 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Thu, 31 Aug 2017 16:47:26 -0700 Subject: [PATCH 06/30] Add basic flanker task display --- tasks/flanker.py | 215 +++++++++++++---------------------------------- 1 file changed, 59 insertions(+), 156 deletions(-) diff --git a/tasks/flanker.py b/tasks/flanker.py index 55cb61f..d4ff80a 100644 --- a/tasks/flanker.py +++ b/tasks/flanker.py @@ -1,4 +1,3 @@ -import os import sys import time import pandas as pd @@ -11,13 +10,14 @@ class Flanker(object): - def __init__(self, screen, background, blocks=3): + def __init__(self, screen, background, blocks=1, compatibility=False): # Get the pygame display window self.screen = screen self.background = background # Sets font and font size self.font = pygame.font.SysFont("arial", 30) + self.font_stim = pygame.font.SysFont("arial", 90) # Get screen info self.screen_x = self.screen.get_width() @@ -30,103 +30,58 @@ def __init__(self, screen, background, blocks=3): # Experiment options self.NUM_BLOCKS = blocks - self.FIXATION_DURATION_RANGE = (400, 1600) # Range of fixation times - self.CUE_DURATION = 100 - self.PRE_STIM_FIXATION_DURATION = 400 - self.TARGET_OFFSET = 31 # Stimulus vertical offset - self.FLANKER_DURATION = 1700 - self.FEEDBACK_DURATION = 1000 - self.ITI_MAX = 3500 - - # Specify factor levels, and task timings as used by Fan et al. (2002). - self.CONGRUENCY_LEVELS = ("congruent", "incongruent", 'neutral') - self.CUE_LEVELS = ("nocue", "center", "spatial", 'double') - self.LOCATION_LEVELS = ('top', 'bottom') - self.DIRECTION_LEVELS = ('left', 'right') + self.COMPATIBILITY = compatibility # compatibility condition + self.FIXATION_DURATION = 1000 + self.FLANKER_DURATION = 200 + self.MAX_RESPONSE_TIME = 1000 + self.FEEDBACK_DURATION = 1500 + self.ITI = 1500 + + # Specify factor levels + self.CONGRUENCY_LEVELS = ("congruent", "incongruent") + self.DIRECTION_LEVELS = ("left", "right") # Create level combinations - # Level combinations give us 48 trials. + # Level combinations give us 4 trials. self.combinations = list( - product(self.CONGRUENCY_LEVELS, self.CUE_LEVELS, - self.LOCATION_LEVELS, self.DIRECTION_LEVELS)) - - # Get images - self.base_dir = os.path.dirname(os.path.realpath(__file__)) - self.image_path = os.path.join(self.base_dir, "images", "ANT") - - self.img_left_congruent = pygame.image.load( - os.path.join(self.image_path, 'left_congruent.png')) - self.img_left_incongruent = pygame.image.load( - os.path.join(self.image_path, 'left_incongruent.png')) - self.img_right_congruent = pygame.image.load( - os.path.join(self.image_path, 'right_congruent.png')) - self.img_right_incongruent = pygame.image.load( - os.path.join(self.image_path, 'right_incongruent.png')) - self.img_left_neutral = pygame.image.load( - os.path.join(self.image_path, 'left_neutral.png')) - self.img_right_neutral = pygame.image.load( - os.path.join(self.image_path, 'right_neutral.png')) - - self.img_fixation = pygame.image.load( - os.path.join(self.image_path, 'fixation.png')) - self.img_cue = pygame.image.load( - os.path.join(self.image_path, 'cue.png')) - - # Get image dimensions - self.flanker_h = self.img_left_incongruent.get_rect().height - self.fixation_h = self.img_fixation.get_rect().height + product(self.CONGRUENCY_LEVELS, self.DIRECTION_LEVELS)) # Create output dataframe self.all_data = pd.DataFrame() def create_block(self, block_num, combinations, trial_type): if trial_type == "main": - cur_combinations = combinations * 2 - np.random.shuffle(cur_combinations) + cur_combinations = combinations * 30 # 120 total trials else: - np.random.shuffle(combinations) - cur_combinations = combinations[:len(combinations)//2] + cur_combinations = combinations * 5 # 20 practice trials - # Add combinations to dataframe - cur_block = pd.DataFrame(data=cur_combinations, columns=( - 'congruency', 'cue', 'location', 'direction')) + # Add shuffled combinations to dataframe + np.random.shuffle(cur_combinations) + cur_block = pd.DataFrame(data=cur_combinations, + columns=('congruency', 'direction')) # Add timing info to dataframe cur_block["block"] = block_num + 1 - cur_block["fixationTime"] = [x for x in np.random.randint( - self.FIXATION_DURATION_RANGE[0], self.FIXATION_DURATION_RANGE[1], - len(cur_combinations))] return cur_block - def display_flanker(self, flanker_type, location, direction): + def display_flanker(self, flanker_type, direction): # Left flanker if direction == "left": if flanker_type == "congruent": - stimulus = self.img_left_congruent - elif flanker_type == "incongruent": - stimulus = self.img_left_incongruent + stimulus = "< < < < <" else: - stimulus = self.img_left_neutral + stimulus = "> > < > >" # Right flanker else: if flanker_type == "congruent": - stimulus = self.img_right_congruent - elif flanker_type == "incongruent": - stimulus = self.img_right_incongruent + stimulus = "> > > > >" else: - stimulus = self.img_right_neutral - - # Offset the flanker stimulus to above/below fixation - if location == "top": - display.image( - self.screen, stimulus, "center", - self.screen_y/2 - self.flanker_h - self.TARGET_OFFSET) - elif location == "bottom": - display.image(self.screen, stimulus, "center", - self.screen_y/2 + self.TARGET_OFFSET) - - def display_trial(self, trial_num, data, trial_type): + stimulus = "< < > < <" + + display.text(self.screen, self.font_stim, stimulus, "center", "center") + + def display_trial(self, trial_num, data): # Check for a quit press after stimulus was shown for event in pygame.event.get(): if event.type == KEYDOWN and event.key == K_F12: @@ -134,73 +89,22 @@ def display_trial(self, trial_num, data, trial_type): # Display fixation self.screen.blit(self.background, (0, 0)) - display.image(self.screen, self.img_fixation, "center", "center") - pygame.display.flip() - - display.wait(data["fixationTime"][trial_num]) - - # Display cue - self.screen.blit(self.background, (0, 0)) - - cue_type = data["cue"][trial_num] - - if cue_type == "nocue": - # Display fixation in the center - display.image(self.screen, self.img_fixation, "center", "center") - elif cue_type == "center": - # Display cue in the center - display.image(self.screen, self.img_cue, "center", "center") - elif cue_type == "double": - # Display fixation in the center - display.image(self.screen, self.img_fixation, "center", "center") - - # Display cue above and below fixation - display.image( - self.screen, self.img_cue, "center", - self.screen_y/2 - self.fixation_h - self.TARGET_OFFSET) - display.image(self.screen, self.img_cue, - "center", self.screen_y/2 + self.TARGET_OFFSET) - elif cue_type == "spatial": - cue_location = data["location"][trial_num] - - # Display fixation in the center - display.image(self.screen, self.img_fixation, "center", "center") - - # Display cue at target location - if cue_location == "top": - display.image( - self.screen, self.img_cue, "center", - self.screen_y/2 - self.fixation_h - self.TARGET_OFFSET) - elif cue_location == "bottom": - display.image(self.screen, self.img_cue, "center", - self.screen_y/2 + self.TARGET_OFFSET) - + display.text(self.screen, self.font, "+", "center", "center") pygame.display.flip() - # Display cue for certain duration - display.wait(self.CUE_DURATION) + display.wait(self.FIXATION_DURATION) - # Prestim interval with fixation + # Display flanker stimulus self.screen.blit(self.background, (0, 0)) - display.image(self.screen, self.img_fixation, "center", "center") - pygame.display.flip() - - display.wait(self.PRE_STIM_FIXATION_DURATION) - - # Display flanker target - self.screen.blit(self.background, (0, 0)) - display.image(self.screen, self.img_fixation, "center", "center") - self.display_flanker(data["congruency"][trial_num], - data["location"][trial_num], data["direction"][trial_num]) pygame.display.flip() - start_time = int(round(time.time() * 1000)) - # Clear the event queue before checking for responses + start_time = int(round(time.time() * 1000)) pygame.event.clear() response = "NA" + too_slow = False wait_response = True while wait_response: for event in pygame.event.get(): @@ -215,9 +119,14 @@ def display_trial(self, trial_num, data, trial_type): end_time = int(round(time.time() * 1000)) - # If time limit has been reached, consider it a missed trial if end_time - start_time >= self.FLANKER_DURATION: + self.screen.blit(self.background, (0, 0)) + pygame.display.flip() + + if end_time - start_time >= self.MAX_RESPONSE_TIME: + # If time limit has been reached, consider it a missed trial wait_response = False + too_slow = True # Store reaction time and response rt = int(round(time.time() * 1000)) - start_time @@ -227,35 +136,34 @@ def display_trial(self, trial_num, data, trial_type): correct = 1 if response == data["direction"][trial_num] else 0 data.set_value(trial_num, 'correct', correct) - # Display feedback if practice trials - if trial_type == "practice": - self.screen.blit(self.background, (0, 0)) + # Display feedback + self.screen.blit(self.background, (0, 0)) + if too_slow: + display.text(self.screen, self.font, "too slow", + "center", "center", (0, 0, 0)) + else: if correct == 1: - display.text(self.screen, self.font, "correct", + display.text(self.screen, self.font, "right", "center", "center", (0, 255, 0)) else: - display.text(self.screen, self.font, "incorrect", + display.text(self.screen, self.font, "wrong", "center", "center", (255, 0, 0)) - pygame.display.flip() + pygame.display.flip() - display.wait(self.FEEDBACK_DURATION) + display.wait(self.FEEDBACK_DURATION) - # Display fixation during ITI + # Display fixation self.screen.blit(self.background, (0, 0)) - display.image(self.screen, self.img_fixation, "center", "center") + display.text(self.screen, self.font, "+", "center", "center") pygame.display.flip() - - iti = self.ITI_MAX - rt - data["fixationTime"][trial_num] - data.set_value(trial_num, 'ITI', iti) - - display.wait(iti) + display.wait(self.ITI) def run_block(self, block_num, total_blocks, block_type): cur_block = self.create_block( block_num, self.combinations, block_type) for i in range(cur_block.shape[0]): - self.display_trial(i, cur_block, block_type) + self.display_trial(i, cur_block) if block_type == "main": # Add block data to all_data @@ -283,13 +191,12 @@ def run(self): "Keep your eyes on the fixation cross at the " "start of each trial:", 100, self.screen_y/2 - 200) - display.image(self.screen, self.img_fixation, - "center", self.screen_y/2 - 150) + display.text(self.screen, self.font, "+", "center", self.screen_y/2 - 150) display.text(self.screen, self.font, "A set of arrows will appear somewhere on the screen:", 100, self.screen_y/2 - 100) - display.image(self.screen, self.img_left_incongruent, - "center", self.screen_y/2 - 50) + display.text(self.screen, self.font_stim, + "> > < > >", "center", self.screen_y/2 - 50) display.text(self.screen, self.font, "Use the Left / Right arrow keys to indicate " "the direction of the CENTER arrow.", @@ -321,10 +228,7 @@ def run(self): self.screen.blit(self.background, (0, 0)) display.text(self.screen, self.font, "We will now begin the main trials...", - 100, self.screen_y/2 - 50) - display.text(self.screen, self.font, - "You will not receive feedback after each trial.", - 100, self.screen_y/2 + 50) + 100, self.screen_y/2) display.text_space(self.screen, self.font, "center", self.screen_y/2 + 200) pygame.display.flip() @@ -339,8 +243,7 @@ def run(self): self.all_data["trial"] = list(range(1, len(self.all_data) + 1)) # Rearrange the dataframe - columns = ['trial', 'block', 'congruency', 'cue', 'location', - 'fixationTime', 'ITI', 'direction', + columns = ['trial', 'block', 'congruency', 'direction', 'response', 'correct', 'RT'] self.all_data = self.all_data[columns] From 0486d968c902d0a31429c38255f14985e6c45995 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Thu, 31 Aug 2017 16:49:52 -0700 Subject: [PATCH 07/30] Increase flanker display duration --- tasks/flanker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/flanker.py b/tasks/flanker.py index d4ff80a..0e1f30a 100644 --- a/tasks/flanker.py +++ b/tasks/flanker.py @@ -33,7 +33,7 @@ def __init__(self, screen, background, blocks=1, compatibility=False): self.COMPATIBILITY = compatibility # compatibility condition self.FIXATION_DURATION = 1000 self.FLANKER_DURATION = 200 - self.MAX_RESPONSE_TIME = 1000 + self.MAX_RESPONSE_TIME = 1500 self.FEEDBACK_DURATION = 1500 self.ITI = 1500 From 6e6fccf1850801128df7b1e033480fc75f4250b3 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Thu, 31 Aug 2017 17:00:38 -0700 Subject: [PATCH 08/30] Add basic flanker options to Settings window --- designer/settings_window_qt.py | 26 +++++++++++++++------ designer/ui/settings_window_qt.ui | 39 +++++++++++++++++++++++++------ 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/designer/settings_window_qt.py b/designer/settings_window_qt.py index 8db0626..8d2d287 100644 --- a/designer/settings_window_qt.py +++ b/designer/settings_window_qt.py @@ -12,7 +12,7 @@ class Ui_SettingsDialog(object): def setupUi(self, SettingsDialog): SettingsDialog.setObjectName("SettingsDialog") SettingsDialog.setWindowModality(QtCore.Qt.ApplicationModal) - SettingsDialog.resize(415, 320) + SettingsDialog.resize(415, 347) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -29,7 +29,7 @@ def setupUi(self, SettingsDialog): self.scrollArea.setWidgetResizable(True) self.scrollArea.setObjectName("scrollArea") self.scrollAreaWidgetContents = QtWidgets.QWidget() - self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 391, 265)) + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 391, 292)) self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents) self.verticalLayout_5.setContentsMargins(0, 0, 0, 0) @@ -42,7 +42,7 @@ def setupUi(self, SettingsDialog): self.settings_toolbox.setFrameShape(QtWidgets.QFrame.NoFrame) self.settings_toolbox.setObjectName("settings_toolbox") self.general_page = QtWidgets.QWidget() - self.general_page.setGeometry(QtCore.QRect(0, 0, 373, 112)) + self.general_page.setGeometry(QtCore.QRect(0, 0, 373, 139)) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -90,7 +90,7 @@ def setupUi(self, SettingsDialog): self.verticalLayout_7.addItem(spacerItem) self.settings_toolbox.addItem(self.general_page, "") self.ant_page = QtWidgets.QWidget() - self.ant_page.setGeometry(QtCore.QRect(0, 0, 373, 112)) + self.ant_page.setGeometry(QtCore.QRect(0, 0, 373, 139)) self.ant_page.setObjectName("ant_page") self.horizontalLayout = QtWidgets.QHBoxLayout(self.ant_page) self.horizontalLayout.setContentsMargins(0, 0, 0, 0) @@ -107,6 +107,7 @@ def setupUi(self, SettingsDialog): self.horizontalLayout.addLayout(self.ant_form) self.settings_toolbox.addItem(self.ant_page, "") self.flanker_page = QtWidgets.QWidget() + self.flanker_page.setGeometry(QtCore.QRect(0, 0, 373, 139)) self.flanker_page.setObjectName("flanker_page") self.verticalLayout = QtWidgets.QVBoxLayout(self.flanker_page) self.verticalLayout.setContentsMargins(0, 0, 0, 0) @@ -114,10 +115,19 @@ def setupUi(self, SettingsDialog): self.flanker_form = QtWidgets.QFormLayout() self.flanker_form.setContentsMargins(10, -1, 10, -1) self.flanker_form.setObjectName("flanker_form") + self.settings_flanker_compatibility = QtWidgets.QCheckBox(self.flanker_page) + self.settings_flanker_compatibility.setObjectName("settings_flanker_compatibility") + self.flanker_form.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_compatibility) + self.settings_flanker_block_label = QtWidgets.QLabel(self.flanker_page) + self.settings_flanker_block_label.setObjectName("settings_flanker_block_label") + self.flanker_form.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_block_label) + self.settings_flanker_block_value = QtWidgets.QLineEdit(self.flanker_page) + self.settings_flanker_block_value.setObjectName("settings_flanker_block_value") + self.flanker_form.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.settings_flanker_block_value) self.verticalLayout.addLayout(self.flanker_form) self.settings_toolbox.addItem(self.flanker_page, "") self.ravens_page = QtWidgets.QWidget() - self.ravens_page.setGeometry(QtCore.QRect(0, 0, 373, 112)) + self.ravens_page.setGeometry(QtCore.QRect(0, 0, 373, 139)) self.ravens_page.setObjectName("ravens_page") self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.ravens_page) self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) @@ -140,7 +150,7 @@ def setupUi(self, SettingsDialog): self.verticalLayout_4.addLayout(self.formLayout) self.settings_toolbox.addItem(self.ravens_page, "") self.sternberg_page = QtWidgets.QWidget() - self.sternberg_page.setGeometry(QtCore.QRect(0, 0, 373, 112)) + self.sternberg_page.setGeometry(QtCore.QRect(0, 0, 373, 139)) self.sternberg_page.setObjectName("sternberg_page") self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.sternberg_page) self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) @@ -173,7 +183,7 @@ def setupUi(self, SettingsDialog): self.verticalLayout_2.addLayout(self.settings_main_layout) self.retranslateUi(SettingsDialog) - self.settings_toolbox.setCurrentIndex(0) + self.settings_toolbox.setCurrentIndex(2) QtCore.QMetaObject.connectSlotsByName(SettingsDialog) def retranslateUi(self, SettingsDialog): @@ -189,6 +199,8 @@ def retranslateUi(self, SettingsDialog): self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.general_page), _translate("SettingsDialog", "General")) self.settings_ant_blocks_label.setText(_translate("SettingsDialog", "Number of blocks:")) self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.ant_page), _translate("SettingsDialog", "Attention Network Test")) + self.settings_flanker_compatibility.setText(_translate("SettingsDialog", "Compatibility condition")) + self.settings_flanker_block_label.setText(_translate("SettingsDialog", "Number of blocks:")) self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.flanker_page), _translate("SettingsDialog", "Eriksen Flanker Task")) self.settings_ravens_start_label.setText(_translate("SettingsDialog", "Starting image #:")) self.settings_ravens_trials_label.setText(_translate("SettingsDialog", "Number of trials:")) diff --git a/designer/ui/settings_window_qt.ui b/designer/ui/settings_window_qt.ui index b8462cd..13b35b6 100644 --- a/designer/ui/settings_window_qt.ui +++ b/designer/ui/settings_window_qt.ui @@ -10,7 +10,7 @@ 0 0 415 - 320 + 347 @@ -45,7 +45,7 @@ 0 0 391 - 265 + 292 @@ -61,7 +61,7 @@ QFrame::NoFrame - 0 + 2 @@ -69,7 +69,7 @@ 0 0 373 - 112 + 139 @@ -190,7 +190,7 @@ 0 0 373 - 112 + 139 @@ -232,6 +232,14 @@ + + + 0 + 0 + 373 + 139 + + Eriksen Flanker Task @@ -256,6 +264,23 @@ 10 + + + + Compatibility condition + + + + + + + Number of blocks: + + + + + + @@ -266,7 +291,7 @@ 0 0 373 - 112 + 139 @@ -323,7 +348,7 @@ 0 0 373 - 112 + 139 From 4dea0733344d4f5e416f58bf3353d785f5a1527f Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Thu, 31 Aug 2017 17:03:01 -0700 Subject: [PATCH 09/30] Update changelog with settings options --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b6039d..b726a61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Change Log -## [3.2.0](https://github.com/sho-87/cognitive-battery/releases/tag/3.2.0) *(2017-08-xx)* +## [3.2.0](https://github.com/sho-87/cognitive-battery/releases/tag/3.2.0) *(2017-09-xx)* + +**General** +- Added tooltips to the Settings window items to clarify the purpose of each option **Tasks** - Added Eriksen Flanker task From 696fb04488e4ca422b36f715e83cdd4d84d8816a Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Mon, 11 Sep 2017 18:39:16 -0700 Subject: [PATCH 10/30] Add dark mode option --- tasks/flanker.py | 54 +++++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/tasks/flanker.py b/tasks/flanker.py index 0e1f30a..406d958 100644 --- a/tasks/flanker.py +++ b/tasks/flanker.py @@ -10,21 +10,29 @@ class Flanker(object): - def __init__(self, screen, background, blocks=1, compatibility=False): + def __init__(self, screen, background, blocks=1, dark_mode=True, compatibility=False): # Get the pygame display window self.screen = screen self.background = background # Sets font and font size self.font = pygame.font.SysFont("arial", 30) - self.font_stim = pygame.font.SysFont("arial", 90) + self.font_stim = pygame.font.SysFont("arial", 100) + + # Set colours + if dark_mode: + self.colour_bg = (0, 0, 0) + self.colour_font = (255, 255, 255) + else: + self.colour_bg = (255, 255, 255) + self.colour_font = (0, 0, 0) # Get screen info self.screen_x = self.screen.get_width() self.screen_y = self.screen.get_height() # Fill background - self.background.fill((255, 255, 255)) + self.background.fill(self.colour_bg) pygame.display.set_caption("Eriksen Flanker Task") pygame.mouse.set_visible(0) @@ -79,7 +87,7 @@ def display_flanker(self, flanker_type, direction): else: stimulus = "< < > < <" - display.text(self.screen, self.font_stim, stimulus, "center", "center") + display.text(self.screen, self.font_stim, stimulus, "center", "center", self.colour_font) def display_trial(self, trial_num, data): # Check for a quit press after stimulus was shown @@ -89,7 +97,7 @@ def display_trial(self, trial_num, data): # Display fixation self.screen.blit(self.background, (0, 0)) - display.text(self.screen, self.font, "+", "center", "center") + display.text(self.screen, self.font, "+", "center", "center", self.colour_font) pygame.display.flip() display.wait(self.FIXATION_DURATION) @@ -140,7 +148,7 @@ def display_trial(self, trial_num, data): self.screen.blit(self.background, (0, 0)) if too_slow: display.text(self.screen, self.font, "too slow", - "center", "center", (0, 0, 0)) + "center", "center", self.colour_font) else: if correct == 1: display.text(self.screen, self.font, "right", @@ -154,7 +162,7 @@ def display_trial(self, trial_num, data): # Display fixation self.screen.blit(self.background, (0, 0)) - display.text(self.screen, self.font, "+", "center", "center") + display.text(self.screen, self.font, "+", "center", "center", self.colour_font) pygame.display.flip() display.wait(self.ITI) @@ -175,9 +183,9 @@ def run_block(self, block_num, total_blocks, block_type): display.text(self.screen, self.font, "End of current block. " "Start next block when you're ready...", - 100, "center") + 100, "center", self.colour_font) display.text_space(self.screen, self.font, - "center", (self.screen_y/2) + 100) + "center", (self.screen_y/2) + 100, self.colour_font) pygame.display.flip() display.wait_for_space() @@ -186,26 +194,26 @@ def run(self): # Instructions self.screen.blit(self.background, (0, 0)) display.text(self.screen, self.font, "Eriksen Flanker Task", - "center", self.screen_y/2 - 300) + "center", self.screen_y/2 - 300, self.colour_font) display.text(self.screen, self.font, "Keep your eyes on the fixation cross at the " "start of each trial:", - 100, self.screen_y/2 - 200) - display.text(self.screen, self.font, "+", "center", self.screen_y/2 - 150) + 100, self.screen_y/2 - 200, self.colour_font) + display.text(self.screen, self.font, "+", "center", self.screen_y/2 - 150, self.colour_font) display.text(self.screen, self.font, "A set of arrows will appear somewhere on the screen:", - 100, self.screen_y/2 - 100) + 100, self.screen_y/2 - 100, self.colour_font) display.text(self.screen, self.font_stim, - "> > < > >", "center", self.screen_y/2 - 50) + "> > < > >", "center", self.screen_y/2 - 50, self.colour_font) display.text(self.screen, self.font, "Use the Left / Right arrow keys to indicate " "the direction of the CENTER arrow.", - 100, self.screen_y/2 + 50) + 100, self.screen_y/2 + 50, self.colour_font) display.text(self.screen, self.font, "In example above, you should press the Left arrow.", - 100, self.screen_y/2 + 100) + 100, self.screen_y/2 + 100, self.colour_font) display.text_space(self.screen, self.font, - "center", (self.screen_y/2) + 300) + "center", (self.screen_y/2) + 300, self.colour_font) pygame.display.flip() display.wait_for_space() @@ -214,9 +222,9 @@ def run(self): self.screen.blit(self.background, (0, 0)) display.text(self.screen, self.font, "We'll begin with a some practice trials...", - "center", "center") + "center", "center", self.colour_font) display.text_space(self.screen, self.font, - "center", self.screen_y/2 + 100) + "center", self.screen_y/2 + 100, self.colour_font) pygame.display.flip() display.wait_for_space() @@ -228,9 +236,9 @@ def run(self): self.screen.blit(self.background, (0, 0)) display.text(self.screen, self.font, "We will now begin the main trials...", - 100, self.screen_y/2) + 100, self.screen_y/2, self.colour_font) display.text_space(self.screen, self.font, - "center", self.screen_y/2 + 200) + "center", self.screen_y/2 + 200, self.colour_font) pygame.display.flip() display.wait_for_space() @@ -249,9 +257,9 @@ def run(self): # End screen self.screen.blit(self.background, (0, 0)) - display.text(self.screen, self.font, "End of task", "center", "center") + display.text(self.screen, self.font, "End of task", "center", "center", self.colour_font) display.text_space(self.screen, self.font, - "center", self.screen_y/2 + 100) + "center", self.screen_y/2 + 100, self.colour_font) pygame.display.flip() display.wait_for_space() From 7d2dd6c7ad5148121007ee781efd8a64c5779779 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Tue, 12 Sep 2017 12:16:14 -0700 Subject: [PATCH 11/30] Add flanker settings items --- designer/settings_window_qt.py | 70 +++++++++++++++----- designer/ui/settings_window_qt.ui | 106 ++++++++++++++++++++++++------ 2 files changed, 139 insertions(+), 37 deletions(-) diff --git a/designer/settings_window_qt.py b/designer/settings_window_qt.py index 8d2d287..a4f6467 100644 --- a/designer/settings_window_qt.py +++ b/designer/settings_window_qt.py @@ -12,7 +12,7 @@ class Ui_SettingsDialog(object): def setupUi(self, SettingsDialog): SettingsDialog.setObjectName("SettingsDialog") SettingsDialog.setWindowModality(QtCore.Qt.ApplicationModal) - SettingsDialog.resize(415, 347) + SettingsDialog.resize(416, 364) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -29,7 +29,7 @@ def setupUi(self, SettingsDialog): self.scrollArea.setWidgetResizable(True) self.scrollArea.setObjectName("scrollArea") self.scrollAreaWidgetContents = QtWidgets.QWidget() - self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 391, 292)) + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 392, 309)) self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents) self.verticalLayout_5.setContentsMargins(0, 0, 0, 0) @@ -42,7 +42,7 @@ def setupUi(self, SettingsDialog): self.settings_toolbox.setFrameShape(QtWidgets.QFrame.NoFrame) self.settings_toolbox.setObjectName("settings_toolbox") self.general_page = QtWidgets.QWidget() - self.general_page.setGeometry(QtCore.QRect(0, 0, 373, 139)) + self.general_page.setGeometry(QtCore.QRect(0, 0, 374, 199)) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -90,7 +90,7 @@ def setupUi(self, SettingsDialog): self.verticalLayout_7.addItem(spacerItem) self.settings_toolbox.addItem(self.general_page, "") self.ant_page = QtWidgets.QWidget() - self.ant_page.setGeometry(QtCore.QRect(0, 0, 373, 139)) + self.ant_page.setGeometry(QtCore.QRect(0, 0, 374, 199)) self.ant_page.setObjectName("ant_page") self.horizontalLayout = QtWidgets.QHBoxLayout(self.ant_page) self.horizontalLayout.setContentsMargins(0, 0, 0, 0) @@ -107,7 +107,7 @@ def setupUi(self, SettingsDialog): self.horizontalLayout.addLayout(self.ant_form) self.settings_toolbox.addItem(self.ant_page, "") self.flanker_page = QtWidgets.QWidget() - self.flanker_page.setGeometry(QtCore.QRect(0, 0, 373, 139)) + self.flanker_page.setGeometry(QtCore.QRect(0, 0, 374, 156)) self.flanker_page.setObjectName("flanker_page") self.verticalLayout = QtWidgets.QVBoxLayout(self.flanker_page) self.verticalLayout.setContentsMargins(0, 0, 0, 0) @@ -115,19 +115,42 @@ def setupUi(self, SettingsDialog): self.flanker_form = QtWidgets.QFormLayout() self.flanker_form.setContentsMargins(10, -1, 10, -1) self.flanker_form.setObjectName("flanker_form") - self.settings_flanker_compatibility = QtWidgets.QCheckBox(self.flanker_page) - self.settings_flanker_compatibility.setObjectName("settings_flanker_compatibility") - self.flanker_form.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_compatibility) - self.settings_flanker_block_label = QtWidgets.QLabel(self.flanker_page) - self.settings_flanker_block_label.setObjectName("settings_flanker_block_label") - self.flanker_form.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_block_label) - self.settings_flanker_block_value = QtWidgets.QLineEdit(self.flanker_page) - self.settings_flanker_block_value.setObjectName("settings_flanker_block_value") - self.flanker_form.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.settings_flanker_block_value) + self.settings_flanker_dark = QtWidgets.QCheckBox(self.flanker_page) + self.settings_flanker_dark.setObjectName("settings_flanker_dark") + self.flanker_form.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_dark) + self.settings_flanker_compat_label = QtWidgets.QLabel(self.flanker_page) + self.settings_flanker_compat_label.setObjectName("settings_flanker_compat_label") + self.flanker_form.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_compat_label) + self.settings_flanker_compat_value = QtWidgets.QLineEdit(self.flanker_page) + self.settings_flanker_compat_value.setObjectName("settings_flanker_compat_value") + self.flanker_form.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.settings_flanker_compat_value) + self.settings_flanker_incompat_label = QtWidgets.QLabel(self.flanker_page) + self.settings_flanker_incompat_label.setObjectName("settings_flanker_incompat_label") + self.flanker_form.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_incompat_label) + self.settings_flanker_incompat_value = QtWidgets.QLineEdit(self.flanker_page) + self.settings_flanker_incompat_value.setObjectName("settings_flanker_incompat_value") + self.flanker_form.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.settings_flanker_incompat_value) + self.settings_flanker_order_label = QtWidgets.QLabel(self.flanker_page) + self.settings_flanker_order_label.setObjectName("settings_flanker_order_label") + self.flanker_form.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_order_label) + self.settings_flanker_order_group = QtWidgets.QVBoxLayout() + self.settings_flanker_order_group.setObjectName("settings_flanker_order_group") + self.settings_flanker_order_incompat = QtWidgets.QRadioButton(self.flanker_page) + self.settings_flanker_order_incompat.setCheckable(True) + self.settings_flanker_order_incompat.setChecked(True) + self.settings_flanker_order_incompat.setObjectName("settings_flanker_order_incompat") + self.settings_flanker_order_group.addWidget(self.settings_flanker_order_incompat) + self.settings_flanker_order_compat = QtWidgets.QRadioButton(self.flanker_page) + self.settings_flanker_order_compat.setObjectName("settings_flanker_order_compat") + self.settings_flanker_order_group.addWidget(self.settings_flanker_order_compat) + self.settings_flanker_order_choose = QtWidgets.QRadioButton(self.flanker_page) + self.settings_flanker_order_choose.setObjectName("settings_flanker_order_choose") + self.settings_flanker_order_group.addWidget(self.settings_flanker_order_choose) + self.flanker_form.setLayout(3, QtWidgets.QFormLayout.FieldRole, self.settings_flanker_order_group) self.verticalLayout.addLayout(self.flanker_form) self.settings_toolbox.addItem(self.flanker_page, "") self.ravens_page = QtWidgets.QWidget() - self.ravens_page.setGeometry(QtCore.QRect(0, 0, 373, 139)) + self.ravens_page.setGeometry(QtCore.QRect(0, 0, 374, 199)) self.ravens_page.setObjectName("ravens_page") self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.ravens_page) self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) @@ -150,7 +173,7 @@ def setupUi(self, SettingsDialog): self.verticalLayout_4.addLayout(self.formLayout) self.settings_toolbox.addItem(self.ravens_page, "") self.sternberg_page = QtWidgets.QWidget() - self.sternberg_page.setGeometry(QtCore.QRect(0, 0, 373, 139)) + self.sternberg_page.setGeometry(QtCore.QRect(0, 0, 374, 199)) self.sternberg_page.setObjectName("sternberg_page") self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.sternberg_page) self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) @@ -199,8 +222,19 @@ def retranslateUi(self, SettingsDialog): self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.general_page), _translate("SettingsDialog", "General")) self.settings_ant_blocks_label.setText(_translate("SettingsDialog", "Number of blocks:")) self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.ant_page), _translate("SettingsDialog", "Attention Network Test")) - self.settings_flanker_compatibility.setText(_translate("SettingsDialog", "Compatibility condition")) - self.settings_flanker_block_label.setText(_translate("SettingsDialog", "Number of blocks:")) + self.settings_flanker_dark.setToolTip(_translate("SettingsDialog", "Use dark colour scheme. White text on black background")) + self.settings_flanker_dark.setText(_translate("SettingsDialog", "Dark mode")) + self.settings_flanker_compat_label.setText(_translate("SettingsDialog", "Number of compatible blocks:")) + self.settings_flanker_compat_value.setToolTip(_translate("SettingsDialog", "Compatible block: respond in direction of center arrow")) + self.settings_flanker_incompat_label.setText(_translate("SettingsDialog", "Number of incompatible blocks:")) + self.settings_flanker_incompat_value.setToolTip(_translate("SettingsDialog", "Incompatible block: respond in opposite direction to center arrow")) + self.settings_flanker_order_label.setText(_translate("SettingsDialog", "Block order:")) + self.settings_flanker_order_incompat.setToolTip(_translate("SettingsDialog", "Display compatible blocks first")) + self.settings_flanker_order_incompat.setText(_translate("SettingsDialog", "Compatible first")) + self.settings_flanker_order_compat.setToolTip(_translate("SettingsDialog", "Display incompatible blocks first")) + self.settings_flanker_order_compat.setText(_translate("SettingsDialog", "Incompatible first")) + self.settings_flanker_order_choose.setToolTip(_translate("SettingsDialog", "Choose block order when task starts")) + self.settings_flanker_order_choose.setText(_translate("SettingsDialog", "Choose")) self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.flanker_page), _translate("SettingsDialog", "Eriksen Flanker Task")) self.settings_ravens_start_label.setText(_translate("SettingsDialog", "Starting image #:")) self.settings_ravens_trials_label.setText(_translate("SettingsDialog", "Number of trials:")) diff --git a/designer/ui/settings_window_qt.ui b/designer/ui/settings_window_qt.ui index 13b35b6..d7c5ab8 100644 --- a/designer/ui/settings_window_qt.ui +++ b/designer/ui/settings_window_qt.ui @@ -9,8 +9,8 @@ 0 0 - 415 - 347 + 416 + 364 @@ -44,8 +44,8 @@ 0 0 - 391 - 292 + 392 + 309 @@ -68,8 +68,8 @@ 0 0 - 373 - 139 + 374 + 199 @@ -189,8 +189,8 @@ 0 0 - 373 - 139 + 374 + 199 @@ -236,8 +236,8 @@ 0 0 - 373 - 139 + 374 + 156 @@ -265,21 +265,89 @@ 10 - + + + Use dark colour scheme. White text on black background + - Compatibility condition + Dark mode - + - Number of blocks: + Number of compatible blocks: - + + + Compatible block: respond in direction of center arrow + + + + + + + Number of incompatible blocks: + + + + + + + Incompatible block: respond in opposite direction to center arrow + + + + + + + Block order: + + + + + + + + + Display compatible blocks first + + + Compatible first + + + true + + + true + + + + + + + Display incompatible blocks first + + + Incompatible first + + + + + + + Choose block order when task starts + + + Choose + + + + @@ -290,8 +358,8 @@ 0 0 - 373 - 139 + 374 + 199 @@ -347,8 +415,8 @@ 0 0 - 373 - 139 + 374 + 199 From 718db40f24b8125272b6e64d9f168032d3a59bab Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Tue, 12 Sep 2017 17:03:57 -0700 Subject: [PATCH 12/30] Save and load flanker settings from ini file --- designer/settings_window_qt.py | 28 ++++++++--------- designer/ui/settings_window_qt.ui | 14 ++++----- interface/settings_window.py | 52 ++++++++++++++++++++++++++++--- 3 files changed, 68 insertions(+), 26 deletions(-) diff --git a/designer/settings_window_qt.py b/designer/settings_window_qt.py index a4f6467..444f5f9 100644 --- a/designer/settings_window_qt.py +++ b/designer/settings_window_qt.py @@ -42,7 +42,7 @@ def setupUi(self, SettingsDialog): self.settings_toolbox.setFrameShape(QtWidgets.QFrame.NoFrame) self.settings_toolbox.setObjectName("settings_toolbox") self.general_page = QtWidgets.QWidget() - self.general_page.setGeometry(QtCore.QRect(0, 0, 374, 199)) + self.general_page.setGeometry(QtCore.QRect(0, 0, 374, 156)) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -90,7 +90,7 @@ def setupUi(self, SettingsDialog): self.verticalLayout_7.addItem(spacerItem) self.settings_toolbox.addItem(self.general_page, "") self.ant_page = QtWidgets.QWidget() - self.ant_page.setGeometry(QtCore.QRect(0, 0, 374, 199)) + self.ant_page.setGeometry(QtCore.QRect(0, 0, 374, 156)) self.ant_page.setObjectName("ant_page") self.horizontalLayout = QtWidgets.QHBoxLayout(self.ant_page) self.horizontalLayout.setContentsMargins(0, 0, 0, 0) @@ -135,14 +135,14 @@ def setupUi(self, SettingsDialog): self.flanker_form.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_order_label) self.settings_flanker_order_group = QtWidgets.QVBoxLayout() self.settings_flanker_order_group.setObjectName("settings_flanker_order_group") - self.settings_flanker_order_incompat = QtWidgets.QRadioButton(self.flanker_page) - self.settings_flanker_order_incompat.setCheckable(True) - self.settings_flanker_order_incompat.setChecked(True) - self.settings_flanker_order_incompat.setObjectName("settings_flanker_order_incompat") - self.settings_flanker_order_group.addWidget(self.settings_flanker_order_incompat) self.settings_flanker_order_compat = QtWidgets.QRadioButton(self.flanker_page) + self.settings_flanker_order_compat.setCheckable(True) + self.settings_flanker_order_compat.setChecked(True) self.settings_flanker_order_compat.setObjectName("settings_flanker_order_compat") self.settings_flanker_order_group.addWidget(self.settings_flanker_order_compat) + self.settings_flanker_order_incompat = QtWidgets.QRadioButton(self.flanker_page) + self.settings_flanker_order_incompat.setObjectName("settings_flanker_order_incompat") + self.settings_flanker_order_group.addWidget(self.settings_flanker_order_incompat) self.settings_flanker_order_choose = QtWidgets.QRadioButton(self.flanker_page) self.settings_flanker_order_choose.setObjectName("settings_flanker_order_choose") self.settings_flanker_order_group.addWidget(self.settings_flanker_order_choose) @@ -150,7 +150,7 @@ def setupUi(self, SettingsDialog): self.verticalLayout.addLayout(self.flanker_form) self.settings_toolbox.addItem(self.flanker_page, "") self.ravens_page = QtWidgets.QWidget() - self.ravens_page.setGeometry(QtCore.QRect(0, 0, 374, 199)) + self.ravens_page.setGeometry(QtCore.QRect(0, 0, 374, 156)) self.ravens_page.setObjectName("ravens_page") self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.ravens_page) self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) @@ -173,7 +173,7 @@ def setupUi(self, SettingsDialog): self.verticalLayout_4.addLayout(self.formLayout) self.settings_toolbox.addItem(self.ravens_page, "") self.sternberg_page = QtWidgets.QWidget() - self.sternberg_page.setGeometry(QtCore.QRect(0, 0, 374, 199)) + self.sternberg_page.setGeometry(QtCore.QRect(0, 0, 374, 156)) self.sternberg_page.setObjectName("sternberg_page") self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.sternberg_page) self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) @@ -206,7 +206,7 @@ def setupUi(self, SettingsDialog): self.verticalLayout_2.addLayout(self.settings_main_layout) self.retranslateUi(SettingsDialog) - self.settings_toolbox.setCurrentIndex(2) + self.settings_toolbox.setCurrentIndex(0) QtCore.QMetaObject.connectSlotsByName(SettingsDialog) def retranslateUi(self, SettingsDialog): @@ -229,10 +229,10 @@ def retranslateUi(self, SettingsDialog): self.settings_flanker_incompat_label.setText(_translate("SettingsDialog", "Number of incompatible blocks:")) self.settings_flanker_incompat_value.setToolTip(_translate("SettingsDialog", "Incompatible block: respond in opposite direction to center arrow")) self.settings_flanker_order_label.setText(_translate("SettingsDialog", "Block order:")) - self.settings_flanker_order_incompat.setToolTip(_translate("SettingsDialog", "Display compatible blocks first")) - self.settings_flanker_order_incompat.setText(_translate("SettingsDialog", "Compatible first")) - self.settings_flanker_order_compat.setToolTip(_translate("SettingsDialog", "Display incompatible blocks first")) - self.settings_flanker_order_compat.setText(_translate("SettingsDialog", "Incompatible first")) + self.settings_flanker_order_compat.setToolTip(_translate("SettingsDialog", "Display compatible blocks first")) + self.settings_flanker_order_compat.setText(_translate("SettingsDialog", "Compatible first")) + self.settings_flanker_order_incompat.setToolTip(_translate("SettingsDialog", "Display incompatible blocks first")) + self.settings_flanker_order_incompat.setText(_translate("SettingsDialog", "Incompatible first")) self.settings_flanker_order_choose.setToolTip(_translate("SettingsDialog", "Choose block order when task starts")) self.settings_flanker_order_choose.setText(_translate("SettingsDialog", "Choose")) self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.flanker_page), _translate("SettingsDialog", "Eriksen Flanker Task")) diff --git a/designer/ui/settings_window_qt.ui b/designer/ui/settings_window_qt.ui index d7c5ab8..3ef5c0e 100644 --- a/designer/ui/settings_window_qt.ui +++ b/designer/ui/settings_window_qt.ui @@ -61,7 +61,7 @@ QFrame::NoFrame - 2 + 0 @@ -69,7 +69,7 @@ 0 0 374 - 199 + 156 @@ -190,7 +190,7 @@ 0 0 374 - 199 + 156 @@ -312,7 +312,7 @@ - + Display compatible blocks first @@ -328,7 +328,7 @@ - + Display incompatible blocks first @@ -359,7 +359,7 @@ 0 0 374 - 199 + 156 @@ -416,7 +416,7 @@ 0 0 374 - 199 + 156 diff --git a/interface/settings_window.py b/interface/settings_window.py index 190adf6..0dcccf8 100644 --- a/interface/settings_window.py +++ b/interface/settings_window.py @@ -69,6 +69,32 @@ def __init__(self, parent, settings): self.settings_ant_blocks_value.setText(self.ant_blocks) self.settings.endGroup() + # Flanker settings + self.settings.beginGroup("Flanker") + if self.settings.value("darkMode") == "true": + self.flanker_dark = True + else: + self.flanker_dark = False + + self.settings_flanker_dark.setChecked(self.flanker_dark) + + self.flanker_compatible_blocks = str(self.settings.value("blocksCompat")) + self.settings_flanker_compat_value.setText(self.flanker_compatible_blocks) + + self.flanker_incompatible_blocks = str(self.settings.value("blocksIncompat")) + self.settings_flanker_incompat_value.setText(self.flanker_incompatible_blocks) + + self.flanker_order = str(self.settings.value("blockOrder")) + + if self.flanker_order == "compatible": + self.settings_flanker_order_compat.setChecked(True) + elif self.flanker_order == "incompatible": + self.settings_flanker_order_incompat.setChecked(True) + elif self.flanker_order == "choose": + self.settings_flanker_order_choose.setChecked(True) + + self.settings.endGroup() + # Ravens settings self.settings.beginGroup("Ravens") self.ravens_start = str(self.settings.value("startImage")) @@ -127,25 +153,26 @@ def save_settings(self): # Check if Ravens images are in range (cant exceed 36 total) if int(self.settings_ravens_start_value.text()) > (36 - int(self.settings_ravens_trials_value.text()) + 1): QtWidgets.QMessageBox.warning(self, 'Ravens Progressive Matrices Error', - 'Too many images for Ravens task. Start with an earlier image, or use fewer trials') + 'Too many images for Ravens task. ' + 'Start with an earlier image, or use fewer trials') else: # General settings self.settings.beginGroup("GeneralSettings") self.settings.setValue('fullscreen', str(self.settings_task_fullscreen_checkbox.isChecked()).lower()) - ## Only save some options if fullscreen is not selected + # Only save some options if fullscreen is not selected if not self.task_fullscreen: self.settings.setValue( 'borderless', str(self.settings_task_borderless_checkbox.isChecked()).lower()) self.settings.setValue('width', - self.settings_task_width_value.text()) + self.settings_task_width_value.text()) self.settings.setValue('height', - self.settings_task_height_value.text()) + self.settings_task_height_value.text()) - ## Task beep setting + # Task beep setting self.settings.setValue('taskBeep', str(self.settings_task_beep_checkbox.isChecked()).lower()) self.settings.endGroup() @@ -155,6 +182,21 @@ def save_settings(self): self.settings.setValue("numBlocks", self.settings_ant_blocks_value.text()) self.settings.endGroup() + # Flanker settings + self.settings.beginGroup("Flanker") + self.settings.setValue('darkMode', str(self.settings_flanker_dark.isChecked()).lower()) + self.settings.setValue("blocksCompat", self.settings_flanker_compat_value.text()) + self.settings.setValue("blocksIncompat", self.settings_flanker_incompat_value.text()) + + if self.settings_flanker_order_compat.isChecked(): + self.settings.setValue("blockOrder", "compatible") + elif self.settings_flanker_order_incompat.isChecked(): + self.settings.setValue("blockOrder", "incompatible") + elif self.settings_flanker_order_choose.isChecked(): + self.settings.setValue("blockOrder", "choose") + + self.settings.endGroup() + # Ravens settings self.settings.beginGroup("Ravens") self.settings.setValue("startImage", self.settings_ravens_start_value.text()) From 1714c7471b0736df72927869160fc0e86d915037 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Tue, 12 Sep 2017 17:10:53 -0700 Subject: [PATCH 13/30] Add input validator for block numbers --- interface/settings_window.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/interface/settings_window.py b/interface/settings_window.py index 0dcccf8..0b810b9 100644 --- a/interface/settings_window.py +++ b/interface/settings_window.py @@ -111,17 +111,13 @@ def __init__(self, parent, settings): self.settings.endGroup() # Set input validators - self.settings_ant_blocks_value.setValidator( - QtGui.QRegExpValidator(QtCore.QRegExp('[0-9]+'))) - - self.settings_ravens_start_value.setValidator( - QtGui.QRegExpValidator(QtCore.QRegExp('[0-9]+'))) - - self.settings_ravens_trials_value.setValidator( - QtGui.QRegExpValidator(QtCore.QRegExp('[0-9]+'))) - - self.settings_sternberg_blocks_value.setValidator( - QtGui.QRegExpValidator(QtCore.QRegExp('[0-9]+'))) + self.regex_numbers = QtGui.QRegExpValidator(QtCore.QRegExp('[0-9]+')) + self.settings_ant_blocks_value.setValidator(self.regex_numbers) + self.settings_flanker_compat_value.setValidator(self.regex_numbers) + self.settings_flanker_incompat_value.setValidator(self.regex_numbers) + self.settings_ravens_start_value.setValidator(self.regex_numbers) + self.settings_ravens_trials_value.setValidator(self.regex_numbers) + self.settings_sternberg_blocks_value.setValidator(self.regex_numbers) # Set starting toolbox item self.settings_toolbox.setCurrentIndex(0) From 1c0bbc60f39435ef71ebaf96a6e44f1bed40a2ee Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Tue, 12 Sep 2017 17:41:19 -0700 Subject: [PATCH 14/30] Write default settings to file on battery start Fixes #16 --- interface/battery_window.py | 92 +++++++++++++++++++++--------------- interface/settings_window.py | 2 +- 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/interface/battery_window.py b/interface/battery_window.py index 7ab34e7..abc10b6 100644 --- a/interface/battery_window.py +++ b/interface/battery_window.py @@ -42,36 +42,8 @@ def __init__(self, base_dir, project_dir, res_width, res_height): self.settings = QtCore.QSettings(self.settings_file, QtCore.QSettings.IniFormat) self.settings.setFallbacksEnabled(False) - # If first run, store some default settings - # FIXME this is messy when adding new tasks (see issue #16) - if not os.path.isfile(self.settings_file): - # Main window size and position - self.save_main_window_settings(self.size(), QtCore.QPoint(100, 100)) - - # Settings - General - self.settings.beginGroup("GeneralSettings") - self.settings.setValue('fullscreen', "false") - self.settings.setValue('borderless', "false") - self.settings.setValue('width', 1280) - self.settings.setValue('height', 1024) - self.settings.setValue('taskBeep', "true") - self.settings.endGroup() - - # Settings - Attention Network Test - self.settings.beginGroup("AttentionNetworkTest") - self.settings.setValue('numBlocks', 3) - self.settings.endGroup() - - # Settings - Ravens - self.settings.beginGroup("Ravens") - self.settings.setValue('startImage', 13) - self.settings.setValue('numTrials', 12) - self.settings.endGroup() - - # Settings - Sternberg Task - self.settings.beginGroup("Sternberg") - self.settings.setValue('numBlocks', 2) - self.settings.endGroup() + # Read and save default settings + self.set_default_settings() # Set initial window size/pos from saved settings self.settings.beginGroup("MainWindow") @@ -131,6 +103,53 @@ def __init__(self, base_dir, project_dir, res_width, res_height): self.upButton.clicked.connect(self.move_up) self.downButton.clicked.connect(self.move_down) + # Set default settings values + def set_default_settings(self): + """ + Read and save settings values if it exists. Else, write a default value + + This prevents overwriting of existing settings if new task settings are added. + """ + + # Settings - Main Window + self.settings.beginGroup("MainWindow") + self.settings.setValue("size", self.settings.value("size", self.size())) + self.settings.setValue("pos", self.settings.value("pos", QtCore.QPoint(100, 100))) + self.settings.endGroup() + + # Settings - General + self.settings.beginGroup("GeneralSettings") + self.settings.setValue("fullscreen", self.settings.value("fullscreen", "false")) + self.settings.setValue("borderless", self.settings.value("borderless", "false")) + self.settings.setValue("width", self.settings.value("width", 1280)) + self.settings.setValue("height", self.settings.value("height", 1024)) + self.settings.setValue("taskBeep", self.settings.value("taskBeep", "true")) + self.settings.endGroup() + + # Settings - Attention Network Test + self.settings.beginGroup("AttentionNetworkTest") + self.settings.setValue("numBlocks", self.settings.value("numBlocks", 3)) + self.settings.endGroup() + + # Settings - Flanker + self.settings.beginGroup("Flanker") + self.settings.setValue("darkMode", self.settings.value("darkMode", "false")) + self.settings.setValue("blocksCompat", self.settings.value("blocksCompat", 1)) + self.settings.setValue("blocksIncompat", self.settings.value("blocksIncompat", 0)) + self.settings.setValue("blockOrder", self.settings.value("blockOrder", "compatible")) + self.settings.endGroup() + + # Settings - Ravens + self.settings.beginGroup("Ravens") + self.settings.setValue("startImage", self.settings.value("startImage", 13)) + self.settings.setValue("numTrials", self.settings.value("numTrials", 12)) + self.settings.endGroup() + + # Settings - Sternberg Task + self.settings.beginGroup("Sternberg") + self.settings.setValue("numBlocks", self.settings.value("numBlocks", 2)) + self.settings.endGroup() + # Open web browser to the documentation page def show_documentation(self): QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.LINKS["github"])) @@ -213,13 +232,6 @@ def move_down(self): self.taskList.insertItem(current_row + 1, current_item) self.taskList.setCurrentItem(current_item) - # Save window size/position to settings file - def save_main_window_settings(self, size, pos): - self.settings.beginGroup("MainWindow") - self.settings.setValue('size', size) - self.settings.setValue('pos', pos) - self.settings.endGroup() - def get_settings(self): # General settings self.settings.beginGroup("GeneralSettings") @@ -262,7 +274,11 @@ def get_settings(self): # Override the closeEvent method def closeEvent(self, event): - self.save_main_window_settings(self.size(), self.pos()) + # Save window size and position + self.settings.beginGroup("MainWindow") + self.settings.setValue('size', self.size()) + self.settings.setValue('pos', self.pos()) + self.settings.endGroup() event.accept() sys.exit(0) # This closes any open pygame windows diff --git a/interface/settings_window.py b/interface/settings_window.py index 0b810b9..ddfb744 100644 --- a/interface/settings_window.py +++ b/interface/settings_window.py @@ -180,7 +180,7 @@ def save_settings(self): # Flanker settings self.settings.beginGroup("Flanker") - self.settings.setValue('darkMode', str(self.settings_flanker_dark.isChecked()).lower()) + self.settings.setValue("darkMode", str(self.settings_flanker_dark.isChecked()).lower()) self.settings.setValue("blocksCompat", self.settings_flanker_compat_value.text()) self.settings.setValue("blocksIncompat", self.settings_flanker_incompat_value.text()) From 4c5879841bff5fc3842ca45b17a72f00758b4ea1 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Tue, 12 Sep 2017 17:49:55 -0700 Subject: [PATCH 15/30] Move app-wide values to separate module --- CHANGELOG.md | 8 ++++++-- interface/about_dialog.py | 3 ++- interface/battery_window.py | 26 ++++++++------------------ interface/project_window.py | 26 ++++++++------------------ utils/values.py | 18 ++++++++++++++++++ 5 files changed, 42 insertions(+), 39 deletions(-) create mode 100644 utils/values.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b726a61..61cdb4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,14 @@ ## [3.2.0](https://github.com/sho-87/cognitive-battery/releases/tag/3.2.0) *(2017-09-xx)* **General** -- Added tooltips to the Settings window items to clarify the purpose of each option +- Added tooltips to the Settings window items to clarify the purpose of each option. +- App-wide values moved to separate module (e.g. URLs) **Tasks** -- Added Eriksen Flanker task +- Added Eriksen Flanker task. + +**Bug Fixes** +- Fixed a bug where settings for new tasks are not read/saved if `settings.ini` already exists. ## [3.1.0](https://github.com/sho-87/cognitive-battery/releases/tag/3.1.0) *(2017-07-06)* diff --git a/interface/about_dialog.py b/interface/about_dialog.py index 0fd1704..63ba0bc 100644 --- a/interface/about_dialog.py +++ b/interface/about_dialog.py @@ -2,6 +2,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets from designer import about_dialog_qt +from utils import values class AboutDialog(QtWidgets.QDialog, about_dialog_qt.Ui_Dialog): @@ -12,7 +13,7 @@ def __init__(self, parent=None): self.setupUi(self) # Set version number - self.versionValue.setText("3.2.0") + self.versionValue.setText(values.get_version()) # Add icon image base_dir = os.path.dirname(os.path.dirname( diff --git a/interface/battery_window.py b/interface/battery_window.py index abc10b6..79be670 100644 --- a/interface/battery_window.py +++ b/interface/battery_window.py @@ -6,7 +6,7 @@ import pandas as pd from PyQt5 import QtCore, QtGui, QtWidgets -from utils import display +from utils import display, values from designer import battery_window_qt from interface import about_dialog, settings_window from tasks import ant, flanker, mrt, sart, ravens, digitspan_backwards, sternberg @@ -65,17 +65,7 @@ def __init__(self, base_dir, project_dir, res_width, res_height): self.pygame_screen = None # Define URLs - self.LINKS = { - "github": "https://github.com/sho-87/cognitive-battery", - "license": "https://github.com/sho-87/" - "cognitive-battery/blob/master/LICENSE", - "develop": "https://github.com/sho-87/" - "cognitive-battery/tree/develop", - "issues": "https://github.com/sho-87/cognitive-battery/issues", - "new_issue": "https://github.com/sho-87/" - "cognitive-battery/issues/new", - "releases": "https://github.com/sho-87/cognitive-battery/releases" - } + self.links = values.get_links() # Make data folder if it doesnt exist self.dataPath = os.path.join(self.project_dir, "data") @@ -152,27 +142,27 @@ def set_default_settings(self): # Open web browser to the documentation page def show_documentation(self): - QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.LINKS["github"])) + QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.links["github"])) # Open web browser to the license page def show_license(self): - QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.LINKS["license"])) + QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.links["license"])) # Open web browser to the github develop branch for contribution def show_contribute(self): - QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.LINKS["develop"])) + QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.links["develop"])) # Open web browser to the github issues page def show_browse_issues(self): - QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.LINKS["issues"])) + QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.links["issues"])) # Open web browser to the github new issue post def show_new_issue(self): - QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.LINKS["new_issue"])) + QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.links["new_issue"])) # Open web browser to the github releases page def show_releases(self): - QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.LINKS["releases"])) + QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.links["releases"])) # Create a new SettingsWindow object and display it def show_settings(self): diff --git a/interface/project_window.py b/interface/project_window.py index 9885d1c..2864f69 100644 --- a/interface/project_window.py +++ b/interface/project_window.py @@ -1,11 +1,11 @@ import os -import sys import json from datetime import datetime from PyQt5 import QtCore, QtGui, QtWidgets from designer import project_window_qt from interface import about_dialog, battery_window, project_new_window +from utils import values class ProjectWindow(QtWidgets.QMainWindow, project_window_qt.Ui_ProjectWindow): @@ -37,17 +37,7 @@ def __init__(self, base_dir, res_width, res_height): self.base_dir = base_dir # Define URLs - self.LINKS = { - "github": "https://github.com/sho-87/cognitive-battery", - "license": "https://github.com/sho-87/" - "cognitive-battery/blob/master/LICENSE", - "develop": "https://github.com/sho-87/" - "cognitive-battery/tree/develop", - "issues": "https://github.com/sho-87/cognitive-battery/issues", - "new_issue": "https://github.com/sho-87/" - "cognitive-battery/issues/new", - "releases": "https://github.com/sho-87/cognitive-battery/releases" - } + self.links = values.get_links() # Check if project file exists if not os.path.isfile(os.path.join(self.base_dir, 'projects.txt')): @@ -89,27 +79,27 @@ def new_project(self): # Open web browser to the documentation page def show_documentation(self): - QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.LINKS["github"])) + QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.links["github"])) # Open web browser to the license page def show_license(self): - QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.LINKS["license"])) + QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.links["license"])) # Open web browser to the github develop branch for contribution def show_contribute(self): - QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.LINKS["develop"])) + QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.links["develop"])) # Open web browser to the github issues page def show_browse_issues(self): - QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.LINKS["issues"])) + QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.links["issues"])) # Open web browser to the github new issue post def show_new_issue(self): - QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.LINKS["new_issue"])) + QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.links["new_issue"])) # Open web browser to the github releases page def show_releases(self): - QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.LINKS["releases"])) + QtGui.QDesktopServices.openUrl(QtCore.QUrl(self.links["releases"])) # Create a new AboutDialog object and display it def show_about(self): diff --git a/utils/values.py b/utils/values.py new file mode 100644 index 0000000..e1cc5b2 --- /dev/null +++ b/utils/values.py @@ -0,0 +1,18 @@ +def get_version(): + return "3.2.0" + + +def get_links(): + links = { + "github": "https://github.com/sho-87/cognitive-battery", + "license": "https://github.com/sho-87/" + "cognitive-battery/blob/master/LICENSE", + "develop": "https://github.com/sho-87/" + "cognitive-battery/tree/develop", + "issues": "https://github.com/sho-87/cognitive-battery/issues", + "new_issue": "https://github.com/sho-87/" + "cognitive-battery/issues/new", + "releases": "https://github.com/sho-87/cognitive-battery/releases" + } + + return links From c235b6b8eb90b3c9dbe6b249088c14f2145f6f71 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Tue, 12 Sep 2017 17:59:07 -0700 Subject: [PATCH 16/30] Add consistency to string format --- interface/about_dialog.py | 2 +- interface/battery_window.py | 64 ++++++++++++++++----------------- interface/project_new_window.py | 15 ++++---- interface/project_window.py | 18 +++++----- interface/settings_window.py | 20 +++++------ tasks/flanker.py | 12 +++---- 6 files changed, 65 insertions(+), 66 deletions(-) diff --git a/interface/about_dialog.py b/interface/about_dialog.py index 63ba0bc..c375197 100644 --- a/interface/about_dialog.py +++ b/interface/about_dialog.py @@ -18,7 +18,7 @@ def __init__(self, parent=None): # Add icon image base_dir = os.path.dirname(os.path.dirname( os.path.abspath(__file__))) # Get parent directory - pixmap = QtGui.QPixmap(os.path.join(base_dir, 'images', 'icon.png')) + pixmap = QtGui.QPixmap(os.path.join(base_dir, "images", "icon.png")) self.icon.setPixmap(pixmap) # Delete the object when dialog is closed diff --git a/interface/battery_window.py b/interface/battery_window.py index 79be670..9599586 100644 --- a/interface/battery_window.py +++ b/interface/battery_window.py @@ -20,9 +20,9 @@ def __init__(self, base_dir, project_dir, res_width, res_height): self.setupUi(self) # Set app icon - self.setWindowIcon(QtGui.QIcon(os.path.join('images', 'icon_sml.png'))) + self.setWindowIcon(QtGui.QIcon(os.path.join("images", "icon_sml.png"))) - self.github_icon = os.path.join('images', 'github_icon.png') + self.github_icon = os.path.join("images", "github_icon.png") self.actionDocumentation.setIcon(QtGui.QIcon(self.github_icon)) self.actionLicense.setIcon(QtGui.QIcon(self.github_icon)) self.actionContribute.setIcon(QtGui.QIcon(self.github_icon)) @@ -171,7 +171,7 @@ def show_settings(self): self.settings_window = settings_window.SettingsWindow(self, self.settings) self.settings_window.show() self.settings_window.finished.connect( - lambda: setattr(self, 'settings_window', None)) + lambda: setattr(self, "settings_window", None)) # If settings window exists, bring it to the front else: self.settings_window.activateWindow() @@ -183,14 +183,14 @@ def show_about(self): if self.about is None: self.about = about_dialog.AboutDialog(self) self.about.show() - self.about.finished.connect(lambda: setattr(self, 'about', None)) + self.about.finished.connect(lambda: setattr(self, "about", None)) # If about dialog exists, bring it to the front else: self.about.activateWindow() self.about.raise_() def error_dialog(self, message): - QtWidgets.QMessageBox.warning(self, 'Error', message) + QtWidgets.QMessageBox.warning(self, "Error", message) def random_order_selected(self): if self.randomOrderCheck.isChecked(): @@ -266,8 +266,8 @@ def get_settings(self): def closeEvent(self, event): # Save window size and position self.settings.beginGroup("MainWindow") - self.settings.setValue('size', self.size()) - self.settings.setValue('pos', self.pos()) + self.settings.setValue("size", self.size()) + self.settings.setValue("pos", self.pos()) self.settings.endGroup() event.accept() @@ -282,9 +282,9 @@ def start(self): current_date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M") if self.maleRadio.isChecked(): - sex = 'male' + sex = "male" else: - sex = 'female' + sex = "female" # Get *selected* tasks and task order selected_tasks = [] @@ -301,38 +301,38 @@ def start(self): # Check for required inputs if not selected_tasks: - self.error_dialog('No tasks selected') + self.error_dialog("No tasks selected") elif not ra: - self.error_dialog('Please enter RA name...') + self.error_dialog("Please enter RA name...") elif not sub_num: - self.error_dialog('Please enter a subject number...') + self.error_dialog("Please enter a subject number...") elif not condition: - self.error_dialog('Please enter a condition number...') + self.error_dialog("Please enter a condition number...") elif not age: - self.error_dialog('Please enter an age...') + self.error_dialog("Please enter an age...") elif not self.maleRadio.isChecked() and not \ self.femaleRadio.isChecked(): - self.error_dialog('Please select a sex...') + self.error_dialog("Please select a sex...") else: # Store subject info into a dataframe subject_info = pd.DataFrame( data=[(str(current_date), str(sub_num), str(condition), int(age), str(sex), str(ra), - ', '.join(selected_tasks))], - columns=['datetime', 'sub_num', 'condition', - 'age', 'sex', 'RA', 'tasks'] + ", ".join(selected_tasks))], + columns=["datetime", "sub_num", "condition", + "age", "sex", "RA", "tasks"] ) # Check if subject number already exists - existing_subs = [x.split('_')[0] for x in os.listdir(self.dataPath)] + existing_subs = [x.split("_")[0] for x in os.listdir(self.dataPath)] if sub_num in existing_subs: - self.error_dialog('Subject number already exists') + self.error_dialog("Subject number already exists") else: # Create the excel writer object and save the file data_file_name = "%s_%s.xls" % (sub_num, condition) output_file = os.path.join(self.dataPath, data_file_name) writer = pd.ExcelWriter(output_file) - subject_info.to_excel(writer, 'info', index=False) + subject_info.to_excel(writer, "info", index=False) writer.save() # Minimize battery UI @@ -346,7 +346,7 @@ def start(self): pos_x = self.res_width // 2 - self.task_width // 2 pos_y = self.res_height // 2 - self.task_height // 2 - os.environ['SDL_VIDEO_WINDOW_POS'] = \ + os.environ["SDL_VIDEO_WINDOW_POS"] = \ "%s, %s" % (str(pos_x), str(pos_y)) # Initialize pygame @@ -354,9 +354,9 @@ def start(self): # Load beep sound beep_sound = pygame.mixer.Sound(os.path.join(self.base_dir, - 'tasks', - 'media', - 'beep_med.wav')) + "tasks", + "media", + "beep_med.wav")) # Set pygame icon image image = os.path.join(self.base_dir, "images", "icon_sml.png") @@ -390,7 +390,7 @@ def start(self): # Run ANT ant_data = ant_task.run() # Save ANT data to excel - ant_data.to_excel(writer, 'ANT', index=False) + ant_data.to_excel(writer, "ANT", index=False) elif task == "Digit Span (backwards)": digitspan_backwards_task = \ digitspan_backwards.DigitspanBackwards( @@ -400,19 +400,19 @@ def start(self): digitspan_backwards_task.run() # Save digit span (backwards) data to excel digitspan_backwards_data.to_excel( - writer, 'Digit span (backwards)', index=False) + writer, "Digit span (backwards)", index=False) elif task == "Eriksen Flanker Task": flanker_task = flanker.Flanker(self.pygame_screen, background) # Run Eriksen Flanker flanker_data = flanker_task.run() # Save flanker data to excel - flanker_data.to_excel(writer, 'Eriksen Flanker', index=False) + flanker_data.to_excel(writer, "Eriksen Flanker", index=False) elif task == "Mental Rotation Task": mrt_task = mrt.MRT(self.pygame_screen, background) # Run MRT mrt_data = mrt_task.run() # Save MRT data to excel - mrt_data.to_excel(writer, 'MRT', index=False) + mrt_data.to_excel(writer, "MRT", index=False) elif task == "Raven's Progressive Matrices": ravens_task = ravens.Ravens( self.pygame_screen, background, @@ -420,7 +420,7 @@ def start(self): # Run Raven's Matrices ravens_data = ravens_task.run() # Save ravens data to excel - ravens_data.to_excel(writer, 'Ravens Matrices', + ravens_data.to_excel(writer, "Ravens Matrices", index=False) elif task == "Sternberg Task": sternberg_task = sternberg.Sternberg( @@ -429,14 +429,14 @@ def start(self): # Run Sternberg Task sternberg_data = sternberg_task.run() # Save sternberg data to excel - sternberg_data.to_excel(writer, 'Sternberg', + sternberg_data.to_excel(writer, "Sternberg", index=False) elif task == "Sustained Attention to Response Task (SART)": sart_task = sart.SART(self.pygame_screen, background) # Run SART sart_data = sart_task.run() # Save SART data to excel - sart_data.to_excel(writer, 'SART', index=False) + sart_data.to_excel(writer, "SART", index=False) # Play beep after each task if self.task_beep: diff --git a/interface/project_new_window.py b/interface/project_new_window.py index 94864b7..147dda0 100644 --- a/interface/project_new_window.py +++ b/interface/project_new_window.py @@ -1,5 +1,4 @@ import os -import sys import json import time @@ -15,7 +14,7 @@ def __init__(self, base_dir, project_list): self.setupUi(self) # Set app icon - self.setWindowIcon(QtGui.QIcon(os.path.join('images', 'icon_sml.png'))) + self.setWindowIcon(QtGui.QIcon(os.path.join("images", "icon_sml.png"))) # Delete the object when dialog is closed self.setAttribute(QtCore.Qt.WA_DeleteOnClose) @@ -29,7 +28,7 @@ def __init__(self, base_dir, project_list): # Set input validators self.projectNameValue.setValidator( - QtGui.QRegExpValidator(QtCore.QRegExp('[A-Za-z0-9 ]+'))) + QtGui.QRegExpValidator(QtCore.QRegExp("[A-Za-z0-9 ]+"))) # Bind button events self.dirSelectButton.clicked.connect(self.select_file) @@ -37,7 +36,7 @@ def __init__(self, base_dir, project_list): self.cancelButton.clicked.connect(self.close) def select_file(self): - self.file_dialog = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select directory for the project') + self.file_dialog = QtWidgets.QFileDialog.getExistingDirectory(self, "Select directory for the project") self.dirValue.setText(self.file_dialog) def create_project(self): @@ -57,7 +56,7 @@ def create_project(self): # Save if project name is not already in use if project_name not in set(saved_projects): - project_info = {'created': time.time(), 'path': dir_path} + project_info = {"created": time.time(), "path": dir_path} try: self.project_list[researcher][project_name] = project_info @@ -65,11 +64,11 @@ def create_project(self): self.project_list[researcher] = {} self.project_list[researcher][project_name] = project_info - with open(os.path.join(self.base_dir, 'projects.txt'), 'w+') as f: + with open(os.path.join(self.base_dir, "projects.txt"), "w+") as f: json.dump(self.project_list, f, indent=4) self.close() else: - QtWidgets.QMessageBox.warning(self, 'Error', 'Project name already exists') + QtWidgets.QMessageBox.warning(self, "Error", "Project name already exists") else: - QtWidgets.QMessageBox.warning(self, 'Error', 'Please complete every field') + QtWidgets.QMessageBox.warning(self, "Error", "Please complete every field") diff --git a/interface/project_window.py b/interface/project_window.py index 2864f69..6bf6405 100644 --- a/interface/project_window.py +++ b/interface/project_window.py @@ -16,9 +16,9 @@ def __init__(self, base_dir, res_width, res_height): self.setupUi(self) # Set app icon - self.setWindowIcon(QtGui.QIcon(os.path.join('images', 'icon_sml.png'))) + self.setWindowIcon(QtGui.QIcon(os.path.join("images", "icon_sml.png"))) - self.github_icon = os.path.join('images', 'github_icon.png') + self.github_icon = os.path.join("images", "github_icon.png") self.actionDocumentation.setIcon(QtGui.QIcon(self.github_icon)) self.actionLicense.setIcon(QtGui.QIcon(self.github_icon)) self.actionContribute.setIcon(QtGui.QIcon(self.github_icon)) @@ -40,7 +40,7 @@ def __init__(self, base_dir, res_width, res_height): self.links = values.get_links() # Check if project file exists - if not os.path.isfile(os.path.join(self.base_dir, 'projects.txt')): + if not os.path.isfile(os.path.join(self.base_dir, "projects.txt")): self.project_list = {} self.save_projects(self.project_list) else: @@ -107,7 +107,7 @@ def show_about(self): if self.about is None: self.about = about_dialog.AboutDialog(self) self.about.show() - self.about.finished.connect(lambda: setattr(self, 'about', None)) + self.about.finished.connect(lambda: setattr(self, "about", None)) # If about dialog exists, bring it to the front else: self.about.activateWindow() @@ -128,10 +128,10 @@ def project_click(self, item): self.dirValue.setText(project_path) if os.path.isdir(project_path): - self.dirValue.setStyleSheet('QLabel {color: black;}') + self.dirValue.setStyleSheet("QLabel {color: black;}") self.dirInvalid.setText("") else: - self.dirValue.setStyleSheet('QLabel {color: red;}') + self.dirValue.setStyleSheet("QLabel {color: red;}") self.dirInvalid.setText("(Error: invalid path)") # Enable buttons and labels @@ -143,7 +143,7 @@ def project_click(self, item): def start(self, event): if not os.path.isdir(self.dirValue.text()): - QtWidgets.QMessageBox.warning(self, 'Error', 'Invalid project path') + QtWidgets.QMessageBox.warning(self, "Error", "Invalid project path") else: self.main_battery = battery_window.BatteryWindow(self.base_dir, self.dirValue.text(), @@ -173,12 +173,12 @@ def delete_project(self): def save_projects(self, projects): # Save current project list to file - with open(os.path.join(self.base_dir, 'projects.txt'), 'w+') as f: + with open(os.path.join(self.base_dir, "projects.txt"), "w+") as f: json.dump(projects, f, indent=4) def refresh_projects(self): # Load most recent saved project list from file - with open(os.path.join(self.base_dir, 'projects.txt'), 'r') as f: + with open(os.path.join(self.base_dir, "projects.txt"), "r") as f: projects = json.load(f) # Clear existing tree widget diff --git a/interface/settings_window.py b/interface/settings_window.py index ddfb744..6c3e8d3 100644 --- a/interface/settings_window.py +++ b/interface/settings_window.py @@ -111,7 +111,7 @@ def __init__(self, parent, settings): self.settings.endGroup() # Set input validators - self.regex_numbers = QtGui.QRegExpValidator(QtCore.QRegExp('[0-9]+')) + self.regex_numbers = QtGui.QRegExpValidator(QtCore.QRegExp("[0-9]+")) self.settings_ant_blocks_value.setValidator(self.regex_numbers) self.settings_flanker_compat_value.setValidator(self.regex_numbers) self.settings_flanker_incompat_value.setValidator(self.regex_numbers) @@ -142,34 +142,34 @@ def task_fullscreen_checkbox(self): def save_window_information(self): self.settings.beginGroup("SettingsWindow") - self.settings.setValue('size', self.size()) + self.settings.setValue("size", self.size()) self.settings.endGroup() def save_settings(self): # Check if Ravens images are in range (cant exceed 36 total) if int(self.settings_ravens_start_value.text()) > (36 - int(self.settings_ravens_trials_value.text()) + 1): - QtWidgets.QMessageBox.warning(self, 'Ravens Progressive Matrices Error', - 'Too many images for Ravens task. ' - 'Start with an earlier image, or use fewer trials') + QtWidgets.QMessageBox.warning(self, "Ravens Progressive Matrices Error", + "Too many images for Ravens task. " + "Start with an earlier image, or use fewer trials") else: # General settings self.settings.beginGroup("GeneralSettings") - self.settings.setValue('fullscreen', + self.settings.setValue("fullscreen", str(self.settings_task_fullscreen_checkbox.isChecked()).lower()) # Only save some options if fullscreen is not selected if not self.task_fullscreen: self.settings.setValue( - 'borderless', + "borderless", str(self.settings_task_borderless_checkbox.isChecked()).lower()) - self.settings.setValue('width', + self.settings.setValue("width", self.settings_task_width_value.text()) - self.settings.setValue('height', + self.settings.setValue("height", self.settings_task_height_value.text()) # Task beep setting - self.settings.setValue('taskBeep', + self.settings.setValue("taskBeep", str(self.settings_task_beep_checkbox.isChecked()).lower()) self.settings.endGroup() diff --git a/tasks/flanker.py b/tasks/flanker.py index 406d958..792e2ec 100644 --- a/tasks/flanker.py +++ b/tasks/flanker.py @@ -66,7 +66,7 @@ def create_block(self, block_num, combinations, trial_type): # Add shuffled combinations to dataframe np.random.shuffle(cur_combinations) cur_block = pd.DataFrame(data=cur_combinations, - columns=('congruency', 'direction')) + columns=("congruency", "direction")) # Add timing info to dataframe cur_block["block"] = block_num + 1 @@ -138,11 +138,11 @@ def display_trial(self, trial_num, data): # Store reaction time and response rt = int(round(time.time() * 1000)) - start_time - data.set_value(trial_num, 'RT', rt) - data.set_value(trial_num, 'response', response) + data.set_value(trial_num, "RT", rt) + data.set_value(trial_num, "response", response) correct = 1 if response == data["direction"][trial_num] else 0 - data.set_value(trial_num, 'correct', correct) + data.set_value(trial_num, "correct", correct) # Display feedback self.screen.blit(self.background, (0, 0)) @@ -251,8 +251,8 @@ def run(self): self.all_data["trial"] = list(range(1, len(self.all_data) + 1)) # Rearrange the dataframe - columns = ['trial', 'block', 'congruency', 'direction', - 'response', 'correct', 'RT'] + columns = ["trial", "block", "congruency", "direction", + "response", "correct", "RT"] self.all_data = self.all_data[columns] # End screen From c403d8cb09acdcbde10eeb565fa1bdf6806603db Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Tue, 12 Sep 2017 18:14:39 -0700 Subject: [PATCH 17/30] Pass settings into Flanker task --- interface/battery_window.py | 16 +++++++++++++++- tasks/flanker.py | 6 +++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/interface/battery_window.py b/interface/battery_window.py index 9599586..016f944 100644 --- a/interface/battery_window.py +++ b/interface/battery_window.py @@ -251,6 +251,18 @@ def get_settings(self): self.ant_blocks = int(self.settings.value("numBlocks")) self.settings.endGroup() + # Flanker settings + self.settings.beginGroup("Flanker") + if self.settings.value("darkMode") == "true": + self.flanker_dark_mode = True + else: + self.flanker_dark_mode = False + + self.flanker_blocks_compat = int(self.settings.value("blocksCompat")) + self.flanker_blocks_incompat = int(self.settings.value("blocksIncompat")) + self.flanker_block_order = str(self.settings.value("blockOrder")) + self.settings.endGroup() + # Ravens settings self.settings.beginGroup("Ravens") self.ravens_start = int(self.settings.value("startImage")) @@ -402,7 +414,9 @@ def start(self): digitspan_backwards_data.to_excel( writer, "Digit span (backwards)", index=False) elif task == "Eriksen Flanker Task": - flanker_task = flanker.Flanker(self.pygame_screen, background) + flanker_task = flanker.Flanker(self.pygame_screen, background, self.flanker_dark_mode, + self.flanker_blocks_compat, self.flanker_blocks_incompat, + self.flanker_block_order) # Run Eriksen Flanker flanker_data = flanker_task.run() # Save flanker data to excel diff --git a/tasks/flanker.py b/tasks/flanker.py index 792e2ec..c2c92a0 100644 --- a/tasks/flanker.py +++ b/tasks/flanker.py @@ -10,7 +10,8 @@ class Flanker(object): - def __init__(self, screen, background, blocks=1, dark_mode=True, compatibility=False): + def __init__(self, screen, background, dark_mode=False, + blocks_compat=1, blocks_incompat=0, block_order="compatible"): # Get the pygame display window self.screen = screen self.background = background @@ -37,8 +38,7 @@ def __init__(self, screen, background, blocks=1, dark_mode=True, compatibility=F pygame.mouse.set_visible(0) # Experiment options - self.NUM_BLOCKS = blocks - self.COMPATIBILITY = compatibility # compatibility condition + self.NUM_BLOCKS = 1 self.FIXATION_DURATION = 1000 self.FLANKER_DURATION = 200 self.MAX_RESPONSE_TIME = 1500 From 5c2b36fbdd21d45601d50cd897360d1cf7c22f6e Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Tue, 12 Sep 2017 18:22:15 -0700 Subject: [PATCH 18/30] Move flanker stim to dictionary --- tasks/flanker.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/tasks/flanker.py b/tasks/flanker.py index c2c92a0..25c6fe5 100644 --- a/tasks/flanker.py +++ b/tasks/flanker.py @@ -45,6 +45,12 @@ def __init__(self, screen, background, dark_mode=False, self.FEEDBACK_DURATION = 1500 self.ITI = 1500 + # Set stimuli + self.flanker_stim = {"left": {"congruent": "< < < < <", + "incongruent": "> > < > >"}, + "right": {"congruent": "> > > > >", + "incongruent": "< < > < <"}} + # Specify factor levels self.CONGRUENCY_LEVELS = ("congruent", "incongruent") self.DIRECTION_LEVELS = ("left", "right") @@ -74,19 +80,7 @@ def create_block(self, block_num, combinations, trial_type): return cur_block def display_flanker(self, flanker_type, direction): - # Left flanker - if direction == "left": - if flanker_type == "congruent": - stimulus = "< < < < <" - else: - stimulus = "> > < > >" - # Right flanker - else: - if flanker_type == "congruent": - stimulus = "> > > > >" - else: - stimulus = "< < > < <" - + stimulus = self.flanker_stim[direction][flanker_type] display.text(self.screen, self.font_stim, stimulus, "center", "center", self.colour_font) def display_trial(self, trial_num, data): @@ -151,10 +145,10 @@ def display_trial(self, trial_num, data): "center", "center", self.colour_font) else: if correct == 1: - display.text(self.screen, self.font, "right", + display.text(self.screen, self.font, "correct", "center", "center", (0, 255, 0)) else: - display.text(self.screen, self.font, "wrong", + display.text(self.screen, self.font, "incorrect", "center", "center", (255, 0, 0)) pygame.display.flip() @@ -203,8 +197,8 @@ def run(self): display.text(self.screen, self.font, "A set of arrows will appear somewhere on the screen:", 100, self.screen_y/2 - 100, self.colour_font) - display.text(self.screen, self.font_stim, - "> > < > >", "center", self.screen_y/2 - 50, self.colour_font) + display.text(self.screen, self.font_stim, self.flanker_stim["left"]["incongruent"], + "center", self.screen_y/2 - 50, self.colour_font) display.text(self.screen, self.font, "Use the Left / Right arrow keys to indicate " "the direction of the CENTER arrow.", From fca8101f8b9cafaf410a999d5aa7aa50d34507ab Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Tue, 12 Sep 2017 18:38:23 -0700 Subject: [PATCH 19/30] Add choice screen at start of task --- tasks/flanker.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/tasks/flanker.py b/tasks/flanker.py index 25c6fe5..8368703 100644 --- a/tasks/flanker.py +++ b/tasks/flanker.py @@ -38,6 +38,7 @@ def __init__(self, screen, background, dark_mode=False, pygame.mouse.set_visible(0) # Experiment options + self.BLOCK_ORDER = block_order self.NUM_BLOCKS = 1 self.FIXATION_DURATION = 1000 self.FLANKER_DURATION = 200 @@ -57,17 +58,16 @@ def __init__(self, screen, background, dark_mode=False, # Create level combinations # Level combinations give us 4 trials. - self.combinations = list( - product(self.CONGRUENCY_LEVELS, self.DIRECTION_LEVELS)) + self.combinations = list(product(self.CONGRUENCY_LEVELS, self.DIRECTION_LEVELS)) # Create output dataframe self.all_data = pd.DataFrame() def create_block(self, block_num, combinations, trial_type): if trial_type == "main": - cur_combinations = combinations * 30 # 120 total trials + cur_combinations = combinations * 1 # 30 - 120 total trials else: - cur_combinations = combinations * 5 # 20 practice trials + cur_combinations = combinations * 1 # 5 - 20 practice trials # Add shuffled combinations to dataframe np.random.shuffle(cur_combinations) @@ -161,8 +161,7 @@ def display_trial(self, trial_num, data): display.wait(self.ITI) def run_block(self, block_num, total_blocks, block_type): - cur_block = self.create_block( - block_num, self.combinations, block_type) + cur_block = self.create_block(block_num, self.combinations, block_type) for i in range(cur_block.shape[0]): self.display_trial(i, cur_block) @@ -185,6 +184,30 @@ def run_block(self, block_num, total_blocks, block_type): display.wait_for_space() def run(self): + if self.BLOCK_ORDER == "choose": + self.screen.blit(self.background, (0, 0)) + display.text(self.screen, self.font, "Choose block order:", + 100, self.screen_y/2 - 300, self.colour_font) + display.text(self.screen, self.font, + "1 - Compatible first", + 100, self.screen_y/2 - 200, self.colour_font) + display.text(self.screen, self.font, + "2 - Incompatible first", + 100, self.screen_y/2 - 150, self.colour_font) + pygame.display.flip() + + wait_response = True + while wait_response: + for event in pygame.event.get(): + if event.type == KEYDOWN and event.key == K_1: + self.BLOCK_ORDER = "compatible" + wait_response = False + elif event.type == KEYDOWN and event.key == K_2: + self.BLOCK_ORDER = "incompatible" + wait_response = False + elif event.type == KEYDOWN and event.key == K_F12: + sys.exit(0) + # Instructions self.screen.blit(self.background, (0, 0)) display.text(self.screen, self.font, "Eriksen Flanker Task", From 2a83699c15693030d4ce61881649291834fd2de4 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Tue, 12 Sep 2017 22:03:47 -0700 Subject: [PATCH 20/30] Show blocks in correct order --- tasks/flanker.py | 158 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 114 insertions(+), 44 deletions(-) diff --git a/tasks/flanker.py b/tasks/flanker.py index 8368703..2402cb6 100644 --- a/tasks/flanker.py +++ b/tasks/flanker.py @@ -39,7 +39,8 @@ def __init__(self, screen, background, dark_mode=False, # Experiment options self.BLOCK_ORDER = block_order - self.NUM_BLOCKS = 1 + self.BLOCKS_COMPAT = blocks_compat + self.BLOCKS_INCOMPAT = blocks_incompat self.FIXATION_DURATION = 1000 self.FLANKER_DURATION = 200 self.MAX_RESPONSE_TIME = 1500 @@ -63,7 +64,7 @@ def __init__(self, screen, background, dark_mode=False, # Create output dataframe self.all_data = pd.DataFrame() - def create_block(self, block_num, combinations, trial_type): + def create_block(self, block_num, combinations, trial_type, compatibility): if trial_type == "main": cur_combinations = combinations * 1 # 30 - 120 total trials else: @@ -76,6 +77,7 @@ def create_block(self, block_num, combinations, trial_type): # Add timing info to dataframe cur_block["block"] = block_num + 1 + cur_block["compatibility"] = compatibility return cur_block @@ -135,7 +137,10 @@ def display_trial(self, trial_num, data): data.set_value(trial_num, "RT", rt) data.set_value(trial_num, "response", response) - correct = 1 if response == data["direction"][trial_num] else 0 + if data["compatibility"][trial_num] == "compatible": + correct = 1 if response == data["direction"][trial_num] else 0 + else: + correct = 1 if response != data["direction"][trial_num] else 0 data.set_value(trial_num, "correct", correct) # Display feedback @@ -154,14 +159,15 @@ def display_trial(self, trial_num, data): display.wait(self.FEEDBACK_DURATION) - # Display fixation - self.screen.blit(self.background, (0, 0)) - display.text(self.screen, self.font, "+", "center", "center", self.colour_font) - pygame.display.flip() - display.wait(self.ITI) + if trial_num != data.shape[0] - 1: + # Display fixation + self.screen.blit(self.background, (0, 0)) + display.text(self.screen, self.font, "+", "center", "center", self.colour_font) + pygame.display.flip() + display.wait(self.ITI) - def run_block(self, block_num, total_blocks, block_type): - cur_block = self.create_block(block_num, self.combinations, block_type) + def run_block(self, block_num, total_blocks, block_type, compatibility, second_half=False): + cur_block = self.create_block(block_num, self.combinations, block_type, compatibility) for i in range(cur_block.shape[0]): self.display_trial(i, cur_block) @@ -170,6 +176,9 @@ def run_block(self, block_num, total_blocks, block_type): # Add block data to all_data self.all_data = pd.concat([self.all_data, cur_block]) + if second_half: + total_blocks = self.BLOCKS_INCOMPAT + self.BLOCKS_COMPAT + # End of block screen if block_num != total_blocks - 1: # If not the final block self.screen.blit(self.background, (0, 0)) @@ -185,28 +194,40 @@ def run_block(self, block_num, total_blocks, block_type): def run(self): if self.BLOCK_ORDER == "choose": - self.screen.blit(self.background, (0, 0)) - display.text(self.screen, self.font, "Choose block order:", - 100, self.screen_y/2 - 300, self.colour_font) - display.text(self.screen, self.font, - "1 - Compatible first", - 100, self.screen_y/2 - 200, self.colour_font) - display.text(self.screen, self.font, - "2 - Incompatible first", - 100, self.screen_y/2 - 150, self.colour_font) - pygame.display.flip() + # If the order is "choose" but one of the block types has a 0, then dont show choose screen + if self.BLOCKS_COMPAT == 0: + self.BLOCK_ORDER = "incompatible" + elif self.BLOCKS_INCOMPAT == 0: + self.BLOCK_ORDER = "compatible" + else: + self.screen.blit(self.background, (0, 0)) + display.text(self.screen, self.font, "Choose block order:", + 100, self.screen_y/2 - 300, self.colour_font) + display.text(self.screen, self.font, + "1 - Compatible first", + 100, self.screen_y/2 - 200, self.colour_font) + display.text(self.screen, self.font, + "2 - Incompatible first", + 100, self.screen_y/2 - 150, self.colour_font) + pygame.display.flip() - wait_response = True - while wait_response: - for event in pygame.event.get(): - if event.type == KEYDOWN and event.key == K_1: - self.BLOCK_ORDER = "compatible" - wait_response = False - elif event.type == KEYDOWN and event.key == K_2: - self.BLOCK_ORDER = "incompatible" - wait_response = False - elif event.type == KEYDOWN and event.key == K_F12: - sys.exit(0) + wait_response = True + while wait_response: + for event in pygame.event.get(): + if event.type == KEYDOWN and event.key == K_1: + self.BLOCK_ORDER = "compatible" + wait_response = False + elif event.type == KEYDOWN and event.key == K_2: + self.BLOCK_ORDER = "incompatible" + wait_response = False + elif event.type == KEYDOWN and event.key == K_F12: + sys.exit(0) + + # Set block order + if self.BLOCK_ORDER == "compatible": + self.block_type_list = (["compatible"] * self.BLOCKS_COMPAT) + (["incompatible"] * self.BLOCKS_INCOMPAT) + elif self.BLOCK_ORDER == "incompatible": + self.block_type_list = (["incompatible"] * self.BLOCKS_INCOMPAT) + (["compatible"] * self.BLOCKS_COMPAT) # Instructions self.screen.blit(self.background, (0, 0)) @@ -218,17 +239,28 @@ def run(self): 100, self.screen_y/2 - 200, self.colour_font) display.text(self.screen, self.font, "+", "center", self.screen_y/2 - 150, self.colour_font) display.text(self.screen, self.font, - "A set of arrows will appear somewhere on the screen:", + "A set of arrows will appear:", 100, self.screen_y/2 - 100, self.colour_font) display.text(self.screen, self.font_stim, self.flanker_stim["left"]["incongruent"], "center", self.screen_y/2 - 50, self.colour_font) - display.text(self.screen, self.font, - "Use the Left / Right arrow keys to indicate " - "the direction of the CENTER arrow.", - 100, self.screen_y/2 + 50, self.colour_font) - display.text(self.screen, self.font, - "In example above, you should press the Left arrow.", - 100, self.screen_y/2 + 100, self.colour_font) + + if self.block_type_list[0] == "compatible": + display.text(self.screen, self.font, + "Use the Left / Right arrow keys to indicate " + "the direction of the CENTER arrow.", + 100, self.screen_y/2 + 70, self.colour_font) + display.text(self.screen, self.font, + "In example above, you should press the Left arrow.", + 100, self.screen_y/2 + 120, self.colour_font) + elif self.block_type_list[0] == "incompatible": + display.text(self.screen, self.font, + "Use the Left / Right arrow keys to indicate " + "the OPPOSITE direction of the CENTER arrow.", + 100, self.screen_y/2 + 70, self.colour_font) + display.text(self.screen, self.font, + "In example above, you should press the Right arrow.", + 100, self.screen_y/2 + 120, self.colour_font) + display.text_space(self.screen, self.font, "center", (self.screen_y/2) + 300, self.colour_font) pygame.display.flip() @@ -247,7 +279,7 @@ def run(self): display.wait_for_space() # Practice trials - self.run_block(0, 1, "practice") + self.run_block(0, 1, "practice", self.block_type_list[0]) # Instructions Practice End self.screen.blit(self.background, (0, 0)) @@ -260,15 +292,53 @@ def run(self): display.wait_for_space() - # Main task - for i in range(self.NUM_BLOCKS): - self.run_block(i, self.NUM_BLOCKS, "main") + # Main task second half + if self.block_type_list[0] == "compatible": + for i in range(self.BLOCKS_COMPAT): + self.run_block(i, self.BLOCKS_COMPAT, "main", self.block_type_list[0]) + elif self.block_type_list[0] == "incompatible": + for i in range(self.BLOCKS_INCOMPAT): + self.run_block(i, self.BLOCKS_INCOMPAT, "main", self.block_type_list[0]) + + # Second half (if more than one compatibility type) + if self.block_type_list[0] != self.block_type_list[-1]: + # Practice instructions + self.screen.blit(self.background, (0, 0)) + display.text(self.screen, self.font, + "Second half instructions", + 100, self.screen_y/2, self.colour_font) + display.text_space(self.screen, self.font, + "center", self.screen_y/2 + 200, self.colour_font) + pygame.display.flip() + + display.wait_for_space() + + # Practice trials + self.run_block(0, 1, "practice", self.block_type_list[-1]) + + # Instructions Practice End + self.screen.blit(self.background, (0, 0)) + display.text(self.screen, self.font, + "We will now begin the main trials...", + 100, self.screen_y/2, self.colour_font) + display.text_space(self.screen, self.font, + "center", self.screen_y/2 + 200, self.colour_font) + pygame.display.flip() + display.wait_for_space() + + # Main task + if self.block_type_list[-1] == "compatible": + for i in range(self.BLOCKS_COMPAT): + self.run_block(self.BLOCKS_INCOMPAT + i, self.BLOCKS_COMPAT, "main", self.block_type_list[-1], True) + elif self.block_type_list[-1] == "incompatible": + for i in range(self.BLOCKS_INCOMPAT): + self.run_block(self.BLOCKS_COMPAT + i, self.BLOCKS_INCOMPAT, "main", self.block_type_list[-1], True) # Create trial number column self.all_data["trial"] = list(range(1, len(self.all_data) + 1)) # Rearrange the dataframe - columns = ["trial", "block", "congruency", "direction", + columns = ["trial", "block", "compatibility", "congruency", "direction", "response", "correct", "RT"] self.all_data = self.all_data[columns] From 9fb2a14c9170d0ad1a6b3ad34204b23f3dd6b895 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Tue, 12 Sep 2017 22:17:22 -0700 Subject: [PATCH 21/30] Fix RT calculation within the response period --- tasks/flanker.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tasks/flanker.py b/tasks/flanker.py index 2402cb6..e458a01 100644 --- a/tasks/flanker.py +++ b/tasks/flanker.py @@ -66,7 +66,7 @@ def __init__(self, screen, background, dark_mode=False, def create_block(self, block_num, combinations, trial_type, compatibility): if trial_type == "main": - cur_combinations = combinations * 1 # 30 - 120 total trials + cur_combinations = combinations * 4 # 30 - 120 total trials else: cur_combinations = combinations * 1 # 5 - 20 practice trials @@ -105,11 +105,13 @@ def display_trial(self, trial_num, data): pygame.display.flip() # Clear the event queue before checking for responses - start_time = int(round(time.time() * 1000)) pygame.event.clear() response = "NA" too_slow = False wait_response = True + post_flanker_blank_shown = False + + start_time = int(round(time.time() * 1000)) while wait_response: for event in pygame.event.get(): if event.type == KEYDOWN and event.key == K_LEFT: @@ -124,8 +126,10 @@ def display_trial(self, trial_num, data): end_time = int(round(time.time() * 1000)) if end_time - start_time >= self.FLANKER_DURATION: - self.screen.blit(self.background, (0, 0)) - pygame.display.flip() + if not post_flanker_blank_shown: + self.screen.blit(self.background, (0, 0)) + pygame.display.flip() + post_flanker_blank_shown = True if end_time - start_time >= self.MAX_RESPONSE_TIME: # If time limit has been reached, consider it a missed trial From 99be305f97934d7e598bd716d8d3be68618dc435 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Tue, 12 Sep 2017 22:44:25 -0700 Subject: [PATCH 22/30] Add set repeats to Flanker settings --- designer/settings_window_qt.py | 44 +++++++++++++------ designer/ui/settings_window_qt.ui | 72 +++++++++++++++++++++---------- interface/battery_window.py | 5 +++ interface/settings_window.py | 10 +++++ tasks/flanker.py | 7 ++- 5 files changed, 100 insertions(+), 38 deletions(-) diff --git a/designer/settings_window_qt.py b/designer/settings_window_qt.py index 444f5f9..108c0a7 100644 --- a/designer/settings_window_qt.py +++ b/designer/settings_window_qt.py @@ -12,7 +12,7 @@ class Ui_SettingsDialog(object): def setupUi(self, SettingsDialog): SettingsDialog.setObjectName("SettingsDialog") SettingsDialog.setWindowModality(QtCore.Qt.ApplicationModal) - SettingsDialog.resize(416, 364) + SettingsDialog.resize(409, 414) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -29,7 +29,7 @@ def setupUi(self, SettingsDialog): self.scrollArea.setWidgetResizable(True) self.scrollArea.setObjectName("scrollArea") self.scrollAreaWidgetContents = QtWidgets.QWidget() - self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 392, 309)) + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 385, 359)) self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents) self.verticalLayout_5.setContentsMargins(0, 0, 0, 0) @@ -42,7 +42,7 @@ def setupUi(self, SettingsDialog): self.settings_toolbox.setFrameShape(QtWidgets.QFrame.NoFrame) self.settings_toolbox.setObjectName("settings_toolbox") self.general_page = QtWidgets.QWidget() - self.general_page.setGeometry(QtCore.QRect(0, 0, 374, 156)) + self.general_page.setGeometry(QtCore.QRect(0, 0, 367, 206)) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -90,7 +90,7 @@ def setupUi(self, SettingsDialog): self.verticalLayout_7.addItem(spacerItem) self.settings_toolbox.addItem(self.general_page, "") self.ant_page = QtWidgets.QWidget() - self.ant_page.setGeometry(QtCore.QRect(0, 0, 374, 156)) + self.ant_page.setGeometry(QtCore.QRect(0, 0, 367, 206)) self.ant_page.setObjectName("ant_page") self.horizontalLayout = QtWidgets.QHBoxLayout(self.ant_page) self.horizontalLayout.setContentsMargins(0, 0, 0, 0) @@ -107,7 +107,7 @@ def setupUi(self, SettingsDialog): self.horizontalLayout.addLayout(self.ant_form) self.settings_toolbox.addItem(self.ant_page, "") self.flanker_page = QtWidgets.QWidget() - self.flanker_page.setGeometry(QtCore.QRect(0, 0, 374, 156)) + self.flanker_page.setGeometry(QtCore.QRect(0, 0, 367, 206)) self.flanker_page.setObjectName("flanker_page") self.verticalLayout = QtWidgets.QVBoxLayout(self.flanker_page) self.verticalLayout.setContentsMargins(0, 0, 0, 0) @@ -120,19 +120,19 @@ def setupUi(self, SettingsDialog): self.flanker_form.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_dark) self.settings_flanker_compat_label = QtWidgets.QLabel(self.flanker_page) self.settings_flanker_compat_label.setObjectName("settings_flanker_compat_label") - self.flanker_form.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_compat_label) + self.flanker_form.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_compat_label) self.settings_flanker_compat_value = QtWidgets.QLineEdit(self.flanker_page) self.settings_flanker_compat_value.setObjectName("settings_flanker_compat_value") - self.flanker_form.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.settings_flanker_compat_value) + self.flanker_form.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.settings_flanker_compat_value) self.settings_flanker_incompat_label = QtWidgets.QLabel(self.flanker_page) self.settings_flanker_incompat_label.setObjectName("settings_flanker_incompat_label") - self.flanker_form.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_incompat_label) + self.flanker_form.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_incompat_label) self.settings_flanker_incompat_value = QtWidgets.QLineEdit(self.flanker_page) self.settings_flanker_incompat_value.setObjectName("settings_flanker_incompat_value") - self.flanker_form.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.settings_flanker_incompat_value) + self.flanker_form.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.settings_flanker_incompat_value) self.settings_flanker_order_label = QtWidgets.QLabel(self.flanker_page) self.settings_flanker_order_label.setObjectName("settings_flanker_order_label") - self.flanker_form.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_order_label) + self.flanker_form.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_order_label) self.settings_flanker_order_group = QtWidgets.QVBoxLayout() self.settings_flanker_order_group.setObjectName("settings_flanker_order_group") self.settings_flanker_order_compat = QtWidgets.QRadioButton(self.flanker_page) @@ -146,11 +146,23 @@ def setupUi(self, SettingsDialog): self.settings_flanker_order_choose = QtWidgets.QRadioButton(self.flanker_page) self.settings_flanker_order_choose.setObjectName("settings_flanker_order_choose") self.settings_flanker_order_group.addWidget(self.settings_flanker_order_choose) - self.flanker_form.setLayout(3, QtWidgets.QFormLayout.FieldRole, self.settings_flanker_order_group) + self.flanker_form.setLayout(5, QtWidgets.QFormLayout.FieldRole, self.settings_flanker_order_group) + self.settings_flanker_practice_sets_label = QtWidgets.QLabel(self.flanker_page) + self.settings_flanker_practice_sets_label.setObjectName("settings_flanker_practice_sets_label") + self.flanker_form.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_practice_sets_label) + self.settings_flanker_main_sets_label = QtWidgets.QLabel(self.flanker_page) + self.settings_flanker_main_sets_label.setObjectName("settings_flanker_main_sets_label") + self.flanker_form.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.settings_flanker_main_sets_label) + self.settings_flanker_practice_sets_value = QtWidgets.QLineEdit(self.flanker_page) + self.settings_flanker_practice_sets_value.setObjectName("settings_flanker_practice_sets_value") + self.flanker_form.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.settings_flanker_practice_sets_value) + self.settings_flanker_main_sets_value = QtWidgets.QLineEdit(self.flanker_page) + self.settings_flanker_main_sets_value.setObjectName("settings_flanker_main_sets_value") + self.flanker_form.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.settings_flanker_main_sets_value) self.verticalLayout.addLayout(self.flanker_form) self.settings_toolbox.addItem(self.flanker_page, "") self.ravens_page = QtWidgets.QWidget() - self.ravens_page.setGeometry(QtCore.QRect(0, 0, 374, 156)) + self.ravens_page.setGeometry(QtCore.QRect(0, 0, 367, 206)) self.ravens_page.setObjectName("ravens_page") self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.ravens_page) self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) @@ -173,7 +185,7 @@ def setupUi(self, SettingsDialog): self.verticalLayout_4.addLayout(self.formLayout) self.settings_toolbox.addItem(self.ravens_page, "") self.sternberg_page = QtWidgets.QWidget() - self.sternberg_page.setGeometry(QtCore.QRect(0, 0, 374, 156)) + self.sternberg_page.setGeometry(QtCore.QRect(0, 0, 367, 206)) self.sternberg_page.setObjectName("sternberg_page") self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.sternberg_page) self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) @@ -206,7 +218,7 @@ def setupUi(self, SettingsDialog): self.verticalLayout_2.addLayout(self.settings_main_layout) self.retranslateUi(SettingsDialog) - self.settings_toolbox.setCurrentIndex(0) + self.settings_toolbox.setCurrentIndex(2) QtCore.QMetaObject.connectSlotsByName(SettingsDialog) def retranslateUi(self, SettingsDialog): @@ -235,6 +247,10 @@ def retranslateUi(self, SettingsDialog): self.settings_flanker_order_incompat.setText(_translate("SettingsDialog", "Incompatible first")) self.settings_flanker_order_choose.setToolTip(_translate("SettingsDialog", "Choose block order when task starts")) self.settings_flanker_order_choose.setText(_translate("SettingsDialog", "Choose")) + self.settings_flanker_practice_sets_label.setText(_translate("SettingsDialog", "Number of sets per practice block:")) + self.settings_flanker_main_sets_label.setText(_translate("SettingsDialog", "Number of sets per main block:")) + self.settings_flanker_practice_sets_value.setToolTip(_translate("SettingsDialog", "Number of set repeats per practice block. 1 set = 4 trials (all combinations)")) + self.settings_flanker_main_sets_value.setToolTip(_translate("SettingsDialog", "Number of set repeats per main block. 1 set = 4 trials (all combinations)")) self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.flanker_page), _translate("SettingsDialog", "Eriksen Flanker Task")) self.settings_ravens_start_label.setText(_translate("SettingsDialog", "Starting image #:")) self.settings_ravens_trials_label.setText(_translate("SettingsDialog", "Number of trials:")) diff --git a/designer/ui/settings_window_qt.ui b/designer/ui/settings_window_qt.ui index 3ef5c0e..f2a8eaf 100644 --- a/designer/ui/settings_window_qt.ui +++ b/designer/ui/settings_window_qt.ui @@ -9,8 +9,8 @@ 0 0 - 416 - 364 + 409 + 414 @@ -44,8 +44,8 @@ 0 0 - 392 - 309 + 385 + 359 @@ -61,15 +61,15 @@ QFrame::NoFrame - 0 + 2 0 0 - 374 - 156 + 367 + 206 @@ -87,7 +87,7 @@ General - + 6 @@ -189,8 +189,8 @@ 0 0 - 374 - 156 + 367 + 206 @@ -236,8 +236,8 @@ 0 0 - 374 - 156 + 367 + 206 @@ -274,42 +274,42 @@ - + Number of compatible blocks: - + Compatible block: respond in direction of center arrow - + Number of incompatible blocks: - + Incompatible block: respond in opposite direction to center arrow - + Block order: - + @@ -349,6 +349,34 @@ + + + + Number of sets per practice block: + + + + + + + Number of sets per main block: + + + + + + + Number of set repeats per practice block. 1 set = 4 trials (all combinations) + + + + + + + Number of set repeats per main block. 1 set = 4 trials (all combinations) + + + @@ -358,8 +386,8 @@ 0 0 - 374 - 156 + 367 + 206 @@ -415,8 +443,8 @@ 0 0 - 374 - 156 + 367 + 206 diff --git a/interface/battery_window.py b/interface/battery_window.py index 016f944..9c8e997 100644 --- a/interface/battery_window.py +++ b/interface/battery_window.py @@ -124,6 +124,8 @@ def set_default_settings(self): # Settings - Flanker self.settings.beginGroup("Flanker") self.settings.setValue("darkMode", self.settings.value("darkMode", "false")) + self.settings.setValue("setsPractice", self.settings.value("setsPractice", 3)) + self.settings.setValue("setsMain", self.settings.value("setsMain", 25)) self.settings.setValue("blocksCompat", self.settings.value("blocksCompat", 1)) self.settings.setValue("blocksIncompat", self.settings.value("blocksIncompat", 0)) self.settings.setValue("blockOrder", self.settings.value("blockOrder", "compatible")) @@ -258,6 +260,8 @@ def get_settings(self): else: self.flanker_dark_mode = False + self.flanker_sets_practice = int(self.settings.value("setsPractice")) + self.flanker_sets_main = int(self.settings.value("setsMain")) self.flanker_blocks_compat = int(self.settings.value("blocksCompat")) self.flanker_blocks_incompat = int(self.settings.value("blocksIncompat")) self.flanker_block_order = str(self.settings.value("blockOrder")) @@ -415,6 +419,7 @@ def start(self): writer, "Digit span (backwards)", index=False) elif task == "Eriksen Flanker Task": flanker_task = flanker.Flanker(self.pygame_screen, background, self.flanker_dark_mode, + self.flanker_sets_practice, self.flanker_sets_main, self.flanker_blocks_compat, self.flanker_blocks_incompat, self.flanker_block_order) # Run Eriksen Flanker diff --git a/interface/settings_window.py b/interface/settings_window.py index 6c3e8d3..4c826be 100644 --- a/interface/settings_window.py +++ b/interface/settings_window.py @@ -78,6 +78,12 @@ def __init__(self, parent, settings): self.settings_flanker_dark.setChecked(self.flanker_dark) + self.flanker_sets_practice = str(self.settings.value("setsPractice")) + self.settings_flanker_practice_sets_value.setText(self.flanker_sets_practice) + + self.flanker_sets_main = str(self.settings.value("setsMain")) + self.settings_flanker_main_sets_value.setText(self.flanker_sets_main) + self.flanker_compatible_blocks = str(self.settings.value("blocksCompat")) self.settings_flanker_compat_value.setText(self.flanker_compatible_blocks) @@ -113,6 +119,8 @@ def __init__(self, parent, settings): # Set input validators self.regex_numbers = QtGui.QRegExpValidator(QtCore.QRegExp("[0-9]+")) self.settings_ant_blocks_value.setValidator(self.regex_numbers) + self.settings_flanker_practice_sets_value.setValidator(self.regex_numbers) + self.settings_flanker_main_sets_value.setValidator(self.regex_numbers) self.settings_flanker_compat_value.setValidator(self.regex_numbers) self.settings_flanker_incompat_value.setValidator(self.regex_numbers) self.settings_ravens_start_value.setValidator(self.regex_numbers) @@ -181,6 +189,8 @@ def save_settings(self): # Flanker settings self.settings.beginGroup("Flanker") self.settings.setValue("darkMode", str(self.settings_flanker_dark.isChecked()).lower()) + self.settings.setValue("setsPractice", self.settings_flanker_practice_sets_value.text()) + self.settings.setValue("setsMain", self.settings_flanker_main_sets_value.text()) self.settings.setValue("blocksCompat", self.settings_flanker_compat_value.text()) self.settings.setValue("blocksIncompat", self.settings_flanker_incompat_value.text()) diff --git a/tasks/flanker.py b/tasks/flanker.py index e458a01..b258e8c 100644 --- a/tasks/flanker.py +++ b/tasks/flanker.py @@ -11,6 +11,7 @@ class Flanker(object): def __init__(self, screen, background, dark_mode=False, + sets_practice=3, sets_main=25, blocks_compat=1, blocks_incompat=0, block_order="compatible"): # Get the pygame display window self.screen = screen @@ -38,6 +39,8 @@ def __init__(self, screen, background, dark_mode=False, pygame.mouse.set_visible(0) # Experiment options + self.SETS_PRACTICE = sets_practice + self.SETS_MAIN = sets_main self.BLOCK_ORDER = block_order self.BLOCKS_COMPAT = blocks_compat self.BLOCKS_INCOMPAT = blocks_incompat @@ -66,9 +69,9 @@ def __init__(self, screen, background, dark_mode=False, def create_block(self, block_num, combinations, trial_type, compatibility): if trial_type == "main": - cur_combinations = combinations * 4 # 30 - 120 total trials + cur_combinations = combinations * self.SETS_MAIN else: - cur_combinations = combinations * 1 # 5 - 20 practice trials + cur_combinations = combinations * self.SETS_PRACTICE # Add shuffled combinations to dataframe np.random.shuffle(cur_combinations) From 129e5aed0cc9e3d7658cda13601893344c407278 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Tue, 12 Sep 2017 22:49:53 -0700 Subject: [PATCH 23/30] fix formatting --- interface/battery_window.py | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/interface/battery_window.py b/interface/battery_window.py index 9c8e997..4f7e51a 100644 --- a/interface/battery_window.py +++ b/interface/battery_window.py @@ -401,22 +401,18 @@ def start(self): for task in selected_tasks: if task == "Attention Network Test (ANT)": # Set number of blocks for ANT - ant_task = ant.ANT(self.pygame_screen, background, - blocks=self.ant_blocks) + ant_task = ant.ANT(self.pygame_screen, background, blocks=self.ant_blocks) # Run ANT ant_data = ant_task.run() # Save ANT data to excel ant_data.to_excel(writer, "ANT", index=False) elif task == "Digit Span (backwards)": - digitspan_backwards_task = \ - digitspan_backwards.DigitspanBackwards( - self.pygame_screen, background) + digitspan_backwards_task = digitspan_backwards.DigitspanBackwards(self.pygame_screen, + background) # Run Digit span (Backwards) - digitspan_backwards_data = \ - digitspan_backwards_task.run() + digitspan_backwards_data = digitspan_backwards_task.run() # Save digit span (backwards) data to excel - digitspan_backwards_data.to_excel( - writer, "Digit span (backwards)", index=False) + digitspan_backwards_data.to_excel(writer, "Digit span (backwards)", index=False) elif task == "Eriksen Flanker Task": flanker_task = flanker.Flanker(self.pygame_screen, background, self.flanker_dark_mode, self.flanker_sets_practice, self.flanker_sets_main, @@ -433,23 +429,19 @@ def start(self): # Save MRT data to excel mrt_data.to_excel(writer, "MRT", index=False) elif task == "Raven's Progressive Matrices": - ravens_task = ravens.Ravens( - self.pygame_screen, background, - start=self.ravens_start, numTrials=self.ravens_trials) + ravens_task = ravens.Ravens(self.pygame_screen, background, + start=self.ravens_start, numTrials=self.ravens_trials) # Run Raven's Matrices ravens_data = ravens_task.run() # Save ravens data to excel - ravens_data.to_excel(writer, "Ravens Matrices", - index=False) + ravens_data.to_excel(writer, "Ravens Matrices", index=False) elif task == "Sternberg Task": - sternberg_task = sternberg.Sternberg( - self.pygame_screen, background, - blocks=self.sternberg_blocks) + sternberg_task = sternberg.Sternberg(self.pygame_screen, background, + blocks=self.sternberg_blocks) # Run Sternberg Task sternberg_data = sternberg_task.run() # Save sternberg data to excel - sternberg_data.to_excel(writer, "Sternberg", - index=False) + sternberg_data.to_excel(writer, "Sternberg", index=False) elif task == "Sustained Attention to Response Task (SART)": sart_task = sart.SART(self.pygame_screen, background) # Run SART From 08f355750dc97eac6e50a262d3e47733c205cc44 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Wed, 13 Sep 2017 15:56:38 -0700 Subject: [PATCH 24/30] Add second half instructions --- tasks/flanker.py | 69 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/tasks/flanker.py b/tasks/flanker.py index b258e8c..60363db 100644 --- a/tasks/flanker.py +++ b/tasks/flanker.py @@ -51,10 +51,10 @@ def __init__(self, screen, background, dark_mode=False, self.ITI = 1500 # Set stimuli - self.flanker_stim = {"left": {"congruent": "< < < < <", - "incongruent": "> > < > >"}, - "right": {"congruent": "> > > > >", - "incongruent": "< < > < <"}} + self.flanker_stim = {"left": {"congruent": "<<<<<", + "incongruent": ">><>>"}, + "right": {"congruent": ">>>>>", + "incongruent": "<<><<"}} # Specify factor levels self.CONGRUENCY_LEVELS = ("congruent", "incongruent") @@ -249,7 +249,7 @@ def run(self): "A set of arrows will appear:", 100, self.screen_y/2 - 100, self.colour_font) display.text(self.screen, self.font_stim, self.flanker_stim["left"]["incongruent"], - "center", self.screen_y/2 - 50, self.colour_font) + "center", self.screen_y/2 - 60, self.colour_font) if self.block_type_list[0] == "compatible": display.text(self.screen, self.font, @@ -257,7 +257,7 @@ def run(self): "the direction of the CENTER arrow.", 100, self.screen_y/2 + 70, self.colour_font) display.text(self.screen, self.font, - "In example above, you should press the Left arrow.", + "In example above, you should press the LEFT arrow.", 100, self.screen_y/2 + 120, self.colour_font) elif self.block_type_list[0] == "incompatible": display.text(self.screen, self.font, @@ -265,24 +265,26 @@ def run(self): "the OPPOSITE direction of the CENTER arrow.", 100, self.screen_y/2 + 70, self.colour_font) display.text(self.screen, self.font, - "In example above, you should press the Right arrow.", + "In example above, you should press the RIGHT arrow.", 100, self.screen_y/2 + 120, self.colour_font) + display.text(self.screen, self.font, + "Respond as quickly, and as accurately, as you can", + 100, self.screen_y/2 + 200, self.colour_font) + display.text_space(self.screen, self.font, "center", (self.screen_y/2) + 300, self.colour_font) pygame.display.flip() - display.wait_for_space() # Instructions Practice self.screen.blit(self.background, (0, 0)) display.text(self.screen, self.font, - "We'll begin with a some practice trials...", + "We'll begin with some practice trials...", "center", "center", self.colour_font) display.text_space(self.screen, self.font, "center", self.screen_y/2 + 100, self.colour_font) pygame.display.flip() - display.wait_for_space() # Practice trials @@ -296,7 +298,6 @@ def run(self): display.text_space(self.screen, self.font, "center", self.screen_y/2 + 200, self.colour_font) pygame.display.flip() - display.wait_for_space() # Main task second half @@ -309,15 +310,57 @@ def run(self): # Second half (if more than one compatibility type) if self.block_type_list[0] != self.block_type_list[-1]: - # Practice instructions self.screen.blit(self.background, (0, 0)) display.text(self.screen, self.font, - "Second half instructions", + "End of first half. Please inform the experimenter.", 100, self.screen_y/2, self.colour_font) + display.text_space(self.screen, self.font, "center", self.screen_y/2 + 200, self.colour_font) pygame.display.flip() + display.wait_for_space() + # Practice instructions + self.screen.blit(self.background, (0, 0)) + display.text(self.screen, self.font, + "For the second half, the task will be slightly different:", + 100, self.screen_y/2 - 300, self.colour_font) + + display.text(self.screen, self.font_stim, self.flanker_stim["left"]["incongruent"], + "center", self.screen_y/2 - 250, self.colour_font) + + if self.block_type_list[-1] == "compatible": + display.text(self.screen, self.font, + "This time, indicate the direction of the CENTER arrow", + 100, self.screen_y/2 - 100, self.colour_font) + display.text(self.screen, self.font, + "So in the example above, you would press LEFT", + 100, self.screen_y/2, self.colour_font) + elif self.block_type_list[-1] == "incompatible": + display.text(self.screen, self.font, + "This time, indicate the OPPOSITE direction of the CENTER arrow", + 100, self.screen_y/2 - 100, self.colour_font) + display.text(self.screen, self.font, + "So in the example above, you would press RIGHT", + 100, self.screen_y/2, self.colour_font) + + display.text(self.screen, self.font, + "Respond as quickly, and as accurately, as you can", + 100, self.screen_y/2 + 100, self.colour_font) + + display.text_space(self.screen, self.font, + "center", self.screen_y/2 + 250, self.colour_font) + pygame.display.flip() + display.wait_for_space() + + # Instructions Practice + self.screen.blit(self.background, (0, 0)) + display.text(self.screen, self.font, + "We'll begin with some practice trials...", + "center", "center", self.colour_font) + display.text_space(self.screen, self.font, + "center", self.screen_y/2 + 100, self.colour_font) + pygame.display.flip() display.wait_for_space() # Practice trials From f2d7e1244343200850eb09a0452ad831d55952f6 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Wed, 13 Sep 2017 15:57:16 -0700 Subject: [PATCH 25/30] Fix practice instructions grammar --- CHANGELOG.md | 1 + tasks/ant.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61cdb4f..6b76b99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ **General** - Added tooltips to the Settings window items to clarify the purpose of each option. - App-wide values moved to separate module (e.g. URLs) +- Fixed a few typos. **Tasks** - Added Eriksen Flanker task. diff --git a/tasks/ant.py b/tasks/ant.py index fb83180..21d3593 100644 --- a/tasks/ant.py +++ b/tasks/ant.py @@ -306,7 +306,7 @@ def run(self): # Instructions Practice self.screen.blit(self.background, (0, 0)) display.text(self.screen, self.font, - "We'll begin with a some practice trials...", + "We'll begin with some practice trials...", "center", "center") display.text_space(self.screen, self.font, "center", self.screen_y/2 + 100) From 89a877591a61d12f482d59a15d168a77e0e88153 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Wed, 13 Sep 2017 16:16:32 -0700 Subject: [PATCH 26/30] Add flanker task description --- tasks/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tasks/README.md b/tasks/README.md index d9e0d9a..4629cda 100644 --- a/tasks/README.md +++ b/tasks/README.md @@ -24,6 +24,14 @@ - Time between digits: 100ms ## Eriksen Flanker Task +- A single set contains each trial type combination (left/congruent, left/incongruent, right/congruent, right/incongruent) +- 3 practice sets by default, adjustable in settings +- 25 sets in the main blocks, adjustable in settings (~10 minutes) +- Contains options for adding compatible (respond in direction of arrow) and incompatible (respond in opposite direction of arrow) blocks +- 1 compatible block by default +- Flanker stimululs duration: 200ms +- Maximum response time (before timeout): 1500ms +- Inter-trial interval: 1500ms ## Mental Rotation Task (MRT) - 3 practice questions From 46d67824527a9fb355d6924b55aa37ccbe52b336 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Wed, 13 Sep 2017 23:28:22 -0700 Subject: [PATCH 27/30] Widen flanker stimuli --- tasks/flanker.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tasks/flanker.py b/tasks/flanker.py index 60363db..fded69e 100644 --- a/tasks/flanker.py +++ b/tasks/flanker.py @@ -51,10 +51,10 @@ def __init__(self, screen, background, dark_mode=False, self.ITI = 1500 # Set stimuli - self.flanker_stim = {"left": {"congruent": "<<<<<", - "incongruent": ">><>>"}, - "right": {"congruent": ">>>>>", - "incongruent": "<<><<"}} + self.flanker_stim = {"left": {"congruent": "< < < < <", + "incongruent": "> > < > >"}, + "right": {"congruent": "> > > > >", + "incongruent": "< < > < <"}} # Specify factor levels self.CONGRUENCY_LEVELS = ("congruent", "incongruent") From 8456f523b89c53360becc4f596540ce508a9c88a Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Wed, 13 Sep 2017 23:36:11 -0700 Subject: [PATCH 28/30] Clarify instructions --- tasks/flanker.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tasks/flanker.py b/tasks/flanker.py index fded69e..f5d59cc 100644 --- a/tasks/flanker.py +++ b/tasks/flanker.py @@ -254,18 +254,18 @@ def run(self): if self.block_type_list[0] == "compatible": display.text(self.screen, self.font, "Use the Left / Right arrow keys to indicate " - "the direction of the CENTER arrow.", + "the pointing direction of the CENTER arrow.", 100, self.screen_y/2 + 70, self.colour_font) display.text(self.screen, self.font, - "In example above, you should press the LEFT arrow.", + "In example above, you should press the LEFT key.", 100, self.screen_y/2 + 120, self.colour_font) elif self.block_type_list[0] == "incompatible": display.text(self.screen, self.font, "Use the Left / Right arrow keys to indicate " - "the OPPOSITE direction of the CENTER arrow.", + "the OPPOSITE pointing direction of the CENTER arrow.", 100, self.screen_y/2 + 70, self.colour_font) display.text(self.screen, self.font, - "In example above, you should press the RIGHT arrow.", + "In example above, you should press the RIGHT key.", 100, self.screen_y/2 + 120, self.colour_font) display.text(self.screen, self.font, @@ -331,14 +331,14 @@ def run(self): if self.block_type_list[-1] == "compatible": display.text(self.screen, self.font, - "This time, indicate the direction of the CENTER arrow", + "This time, indicate the pointing direction of the CENTER arrow", 100, self.screen_y/2 - 100, self.colour_font) display.text(self.screen, self.font, "So in the example above, you would press LEFT", 100, self.screen_y/2, self.colour_font) elif self.block_type_list[-1] == "incompatible": display.text(self.screen, self.font, - "This time, indicate the OPPOSITE direction of the CENTER arrow", + "This time, indicate the OPPOSITE pointing direction of the CENTER arrow", 100, self.screen_y/2 - 100, self.colour_font) display.text(self.screen, self.font, "So in the example above, you would press RIGHT", From 79e444eea6e671479f169db9662141f411a2de03 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Wed, 13 Sep 2017 23:54:12 -0700 Subject: [PATCH 29/30] Add tooltip hints to settings options Closes #17 --- designer/settings_window_qt.py | 9 +++++++- designer/ui/settings_window_qt.ui | 35 ++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/designer/settings_window_qt.py b/designer/settings_window_qt.py index 108c0a7..69c4701 100644 --- a/designer/settings_window_qt.py +++ b/designer/settings_window_qt.py @@ -218,21 +218,25 @@ def setupUi(self, SettingsDialog): self.verticalLayout_2.addLayout(self.settings_main_layout) self.retranslateUi(SettingsDialog) - self.settings_toolbox.setCurrentIndex(2) + self.settings_toolbox.setCurrentIndex(0) QtCore.QMetaObject.connectSlotsByName(SettingsDialog) def retranslateUi(self, SettingsDialog): _translate = QtCore.QCoreApplication.translate SettingsDialog.setWindowTitle(_translate("SettingsDialog", "Settings")) + self.settings_task_fullscreen_checkbox.setToolTip(_translate("SettingsDialog", "Show tasks in fullscreen mode")) self.settings_task_fullscreen_checkbox.setText(_translate("SettingsDialog", "Fullscreen")) + self.settings_task_borderless_checkbox.setToolTip(_translate("SettingsDialog", "Show tasks in a borderless window")) self.settings_task_borderless_checkbox.setText(_translate("SettingsDialog", "Borderless")) self.settings_task_width_label.setText(_translate("SettingsDialog", "Width:")) self.settings_task_width_value.setStatusTip(_translate("SettingsDialog", "Width of the task windows")) self.settings_task_height_label.setText(_translate("SettingsDialog", "Height:")) self.settings_task_height_value.setStatusTip(_translate("SettingsDialog", "Height of the task windows")) + self.settings_task_beep_checkbox.setToolTip(_translate("SettingsDialog", "Play audible beep at the completion of each task")) self.settings_task_beep_checkbox.setText(_translate("SettingsDialog", "Play beep after each task")) self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.general_page), _translate("SettingsDialog", "General")) self.settings_ant_blocks_label.setText(_translate("SettingsDialog", "Number of blocks:")) + self.settings_ant_blocks_value.setToolTip(_translate("SettingsDialog", "Total number of blocks used in the task")) self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.ant_page), _translate("SettingsDialog", "Attention Network Test")) self.settings_flanker_dark.setToolTip(_translate("SettingsDialog", "Use dark colour scheme. White text on black background")) self.settings_flanker_dark.setText(_translate("SettingsDialog", "Dark mode")) @@ -253,9 +257,12 @@ def retranslateUi(self, SettingsDialog): self.settings_flanker_main_sets_value.setToolTip(_translate("SettingsDialog", "Number of set repeats per main block. 1 set = 4 trials (all combinations)")) self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.flanker_page), _translate("SettingsDialog", "Eriksen Flanker Task")) self.settings_ravens_start_label.setText(_translate("SettingsDialog", "Starting image #:")) + self.settings_ravens_start_value.setToolTip(_translate("SettingsDialog", "First image of the Raven\'s image set to be shown")) self.settings_ravens_trials_label.setText(_translate("SettingsDialog", "Number of trials:")) + self.settings_ravens_trials_value.setToolTip(_translate("SettingsDialog", "Number of total images to show, starting from the start image #")) self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.ravens_page), _translate("SettingsDialog", "Raven\'s Progressive Matrices")) self.settings_sternberg_blocks_label.setText(_translate("SettingsDialog", "Number of blocks:")) + self.settings_sternberg_blocks_value.setToolTip(_translate("SettingsDialog", "Total number of blocks used in the task")) self.settings_toolbox.setItemText(self.settings_toolbox.indexOf(self.sternberg_page), _translate("SettingsDialog", "Sternberg Task")) self.settings_save_button.setStatusTip(_translate("SettingsDialog", "Save settings")) self.settings_save_button.setText(_translate("SettingsDialog", "Save")) diff --git a/designer/ui/settings_window_qt.ui b/designer/ui/settings_window_qt.ui index f2a8eaf..17a9c8e 100644 --- a/designer/ui/settings_window_qt.ui +++ b/designer/ui/settings_window_qt.ui @@ -61,7 +61,7 @@ QFrame::NoFrame - 2 + 0 @@ -116,6 +116,9 @@ + + Show tasks in fullscreen mode + Fullscreen @@ -123,6 +126,9 @@ + + Show tasks in a borderless window + Borderless @@ -162,6 +168,9 @@ + + Play audible beep at the completion of each task + Play beep after each task @@ -225,7 +234,11 @@ - + + + Total number of blocks used in the task + + @@ -422,7 +435,11 @@ - + + + First image of the Raven's image set to be shown + + @@ -432,7 +449,11 @@ - + + + Number of total images to show, starting from the start image # + + @@ -479,7 +500,11 @@ - + + + Total number of blocks used in the task + + From 5850fe28f2d4ee949bb72ab2da930dc63fea9d8c Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Thu, 14 Sep 2017 00:02:44 -0700 Subject: [PATCH 30/30] Update changelog --- CHANGELOG.md | 6 ++++-- LICENSE | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b76b99..5351f74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ # Change Log -## [3.2.0](https://github.com/sho-87/cognitive-battery/releases/tag/3.2.0) *(2017-09-xx)* +## [3.2.0](https://github.com/sho-87/cognitive-battery/releases/tag/3.2.0) *(2017-09-14)* **General** - Added tooltips to the Settings window items to clarify the purpose of each option. -- App-wide values moved to separate module (e.g. URLs) +- App-wide values moved to separate module (e.g. URLs). +- Streamlined the setting of default values for task options. +- Added placeholder scripts for aggregating task data. - Fixed a few typos. **Tasks** diff --git a/LICENSE b/LICENSE index 3171cc7..18f509a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Simon Ho +Copyright (c) 2017 Simon Ho Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal