{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Psychopy Assignment: The stroop effect\n", "\n", "In this exercise we'll put some of what we've learned so far together into an simple experiment demonstrating the Stroop Effect. We'll gradually build this codebase out into a full experiment.\n", "\n", "**General instructions**\n", "- To work on this assignment, please create a GitHub folder called `stroop-experiment` and create a .py file called `stroop.py` using the starter code below.\n", "\n", "- Then, for each task, please create (at least) one separate GitHub commit so that I can see your history of updating your code base.\n", "\n", "- Solution code is provided beneath each exercise, but I encourage you to first try to figure out the code and experiment a bit at each step yourself, knowing that you can always \"check\" the correct answer at any point. The more you practice actually trying to generate (and type out) the code, the more effective the assignment will be at building your psychopy skills." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "remove-output" ] }, "outputs": [], "source": [ "import time\n", "import sys\n", "import os\n", "import random\n", "from psychopy import visual,event,core,gui\n", "\n", "stimuli = ['red', 'orange', 'yellow', 'green', 'blue']\n", "\n", "win = visual.Window([800,600],color=\"gray\", units='pix',checkTiming=False)\n", "placeholder = visual.Rect(win,width=180,height=80, fillColor=\"lightgray\",lineColor=\"black\", lineWidth=6,pos=[0,0])\n", "word_stim = visual.TextStim(win,text=\"\", height=40, color=\"black\",pos=[0,0])\n", "instruction = visual.TextStim(win,text=\"Press the first letter of the ink color\", height=20, color=\"black\",pos=[0,-200])\n", "while True:\n", " cur_stim = random.choice(stimuli)\n", " word_stim.setText(cur_stim)\n", " word_stim.setColor(cur_stim)\n", " placeholder.draw()\n", " instruction.draw()\n", " word_stim.draw()\n", " win.flip()\n", " core.wait(1.0)\n", " placeholder.draw()\n", " instruction.draw() \n", " win.flip()\n", " core.wait(.15)\n", "\n", " if event.getKeys(['q']):\n", " win.close()\n", " core.quit()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once you've successfully run this code and understand what all the lines do, proceed to the following exercises." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## PART 1: BUILD THE BASIC STROOP TRIALS\n", "\n", "1. **Fixation Cross** \n", "Create a fixation cross using a TextStim object `visual.TextStim` set text to \"+\" and color to \"black\" and height to 15. Make the fixation cross appear for 500 ms before each color word, and disappear right before the color word appears, with another 500 ms inter-stimulus interval between the fixation cross and the word." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input", "remove-output" ] }, "outputs": [], "source": [ "#add fixation cross to the top of the script (before the while loop)\n", "fixation = visual.TextStim(win,height=40,color=\"black\",text=\"+\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input", "remove-output" ] }, "outputs": [], "source": [ "#new while loop\n", "#add fixation cross\n", "while True:\n", " cur_stim = random.choice(stimuli)\n", " word_stim.setText(cur_stim)\n", " word_stim.setColor(cur_stim)\n", " placeholder.draw()\n", " instruction.draw()\n", " fixation.draw()\n", " win.flip()\n", " core.wait(.5)\n", " placeholder.draw()\n", " instruction.draw()\n", " win.flip()\n", " core.wait(.5)\n", " placeholder.draw()\n", " instruction.draw()\n", " word_stim.draw()\n", " win.flip()\n", " core.wait(1.0)\n", " placeholder.draw()\n", " instruction.draw() \n", " win.flip()\n", " core.wait(.15)\n", "\n", " if event.getKeys(['q']):\n", " win.close()\n", " core.quit()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "2. **AutoDraw** \n", "\n", "Use the autoDraw functionality to automatically draw the `instruction` for every window flip. This simplifies the code above quite a bit!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input", "remove-output" ] }, "outputs": [], "source": [ "#add autoDraw to instruction\n", "instruction = visual.TextStim(win,text=\"Press the first letter of the ink color\", height=20, color=\"black\",pos=[0,-200],autoDraw=True)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input", "remove-output" ] }, "outputs": [], "source": [ "#new while loop\n", "#remove instruction.draw() since this is now automatically drawn!\n", "while True:\n", " cur_stim = random.choice(stimuli)\n", " word_stim.setText(cur_stim)\n", " word_stim.setColor(cur_stim)\n", " placeholder.draw()\n", " fixation.draw()\n", " win.flip()\n", " core.wait(.5)\n", " placeholder.draw()\n", " win.flip()\n", " core.wait(.5)\n", " placeholder.draw()\n", " word_stim.draw()\n", " win.flip()\n", " core.wait(1.0)\n", " placeholder.draw() \n", " win.flip()\n", " core.wait(.15)\n", "\n", " if event.getKeys(['q']):\n", " win.close()\n", " core.quit()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "3. **Wait for a response**\n", "\n", "Rather than cycling throught the colors, use `event.waitKeys()` to wait for a response (e.g., \"o\" for \"orange\"). Your script should only accept 'r', 'o', 'y', 'g', 'b' (the first letter of each color) and -- to make testing easier for you -- 'q' for quit.\n", "\n", "```{tip}\n", "Make sure your code has just the functionality it needs, e.g., for part 2, you don't need `event.getKeys(['q'])`\n", "```" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input", "remove-output" ] }, "outputs": [], "source": [ "# define valid response keys to the top of the script (before the while loop)\n", "valid_response_keys = ['r', 'o', 'y', 'g', 'b','q']" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input", "remove-output" ] }, "outputs": [], "source": [ "#add event.waitKeys to the while loop\n", "#replace the event.getKeys call with a check of whether the key pressed is a 'q'\n", "while True:\n", " cur_stim = random.choice(stimuli)\n", " word_stim.setText(cur_stim)\n", " word_stim.setColor(cur_stim)\n", " placeholder.draw()\n", " fixation.draw()\n", " win.flip()\n", " core.wait(.5)\n", " placeholder.draw()\n", " win.flip()\n", " core.wait(.5)\n", " placeholder.draw()\n", " word_stim.draw()\n", " win.flip()\n", " key_pressed = event.waitKeys(keyList=valid_response_keys)\n", " print(key_pressed[0])\n", "\n", " if key_pressed[0] == 'q':\n", " break" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "4. **Reaction times** \n", "\n", "Compute the reaction times -- the time it takes to respond from when the color word appears, to when the user presses a key, in milleconds (e.g., .8 secs should show up as 800). Store these in a list called `RTs`. (Use psychopy timers)\n", "\n", "```{tip}\n", "Append each reaction time to the RTs list after every response.\n", "```\n", "\n", "Print the list to verify that the reaction times are correct (e.g., if you take approx 1 second to respond, is it recording 1000?). Your submitted code should have this print statement." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input", "remove-output" ] }, "outputs": [], "source": [ "#update to while loop portion\n", "RTs=[] #set RT list\n", "response_timer = core.Clock() # set response timer clock\n", "key_pressed=False #initializing this for later\n", "while True:\n", " cur_stim = random.choice(stimuli)\n", " word_stim.setText(cur_stim)\n", " word_stim.setColor(cur_stim)\n", " placeholder.draw()\n", " fixation.draw()\n", " win.flip()\n", " core.wait(.5)\n", " placeholder.draw()\n", " win.flip()\n", " core.wait(.5)\n", " placeholder.draw()\n", " word_stim.draw()\n", " win.flip()\n", " response_timer.reset() # immediately after win.flip(), reset clock to measure RT\n", " key_pressed = event.waitKeys(keyList=valid_response_keys)\n", " RTs.append(round(response_timer.getTime()*1000,0)) #add an RT to the list, rounded to the nearest millisecond\n", "\n", " if key_pressed[0] == 'q':\n", " break\n", "\n", "print(RTs)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "5. **Feedback** \n", "\n", "Now let's implement some feedback. If the user responded correctly, do nothing. If the user responded incorrectly, show \"Incorrect\" in black letters and add a 1s time delay before going on to the next trial." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input", "remove-output" ] }, "outputs": [], "source": [ "# add a new feedback TextStim before the while loop\n", "feedback_incorrect = visual.TextStim(win,text=\"INCORRECT\", height=40, color=\"black\",pos=[0,0])" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "remove-output", "hide-input" ] }, "outputs": [], "source": [ "#add feedback after collecting the response and RT\n", "if key_pressed[0] == cur_stim[0]:\n", " #correct response\n", " pass\n", "elif key_pressed[0] == 'q':\n", " break\n", "else:\n", " feedback_incorrect.draw()\n", " win.flip()\n", " core.wait(1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "6. **Timeout** \n", "\n", "Now, instead of waiting for a response forever, let's implement a timeout. Show accuracy feedback as before, show \"Too slow\" if the user takes more than 2 secs to respond." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "remove-output", "hide-input" ] }, "outputs": [], "source": [ "#add 'too slow' feedback before the while loop\n", "feedback_too_slow = visual.TextStim(win,text=\"TOO SLOW\", height=40, color=\"black\",pos=[0,0])" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "remove-output", "hide-input" ] }, "outputs": [], "source": [ "#full while loop\n", "RTs=[] #set RT list\n", "response_timer = core.Clock() # set response timer clock\n", "key_pressed=False #need to initialize this for later\n", "while True:\n", " cur_stim = random.choice(stimuli)\n", " word_stim.setText(cur_stim)\n", " word_stim.setColor(cur_stim)\n", " placeholder.draw()\n", " fixation.draw()\n", " win.flip()\n", " core.wait(.5)\n", " placeholder.draw()\n", " win.flip()\n", " core.wait(.5)\n", " placeholder.draw()\n", " word_stim.draw() \n", " win.flip()\n", " response_timer.reset() # immediately after win.flip(), reset clock to measure RT\n", " key_pressed = event.waitKeys(keyList=valid_response_keys,maxWait=2) # maximum wait time of 2 s\n", " # add feedback\n", " # if key_pressed is still FALSE/ no response was registered, present too slow feedback\n", " if not key_pressed:\n", " feedback_too_slow.draw()\n", " win.flip()\n", " core.wait(1)\n", " elif key_pressed[0] == cur_stim[0]: #elif statement\n", " #correct response\n", " pass\n", " elif key_pressed[0] == 'q':\n", " break\n", " else:\n", " feedback_incorrect.draw()\n", " win.flip()\n", " core.wait(1)\n", " RTs.append(round(response_timer.getTime()*1000,0)) #add an RT to the list, rounded to the nearest millisecond" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "7. **Stroop it up! Add incongruent trials**\n", "\n", "Introduce *incongruent* trials by showing words in the \"wrong\" color, e.g., \"yellow\" printed in green. To do this, define a function called `make_incongruent()` which takes in a color as an argument and returns one of the other colors in `stimuli` that's different from the one being passed in. Then set the color word's color to this new value, thereby creating an incongruent trial. " ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "remove-output", "hide-input" ] }, "outputs": [], "source": [ "#define a function to make colors incongurent\n", "def make_incongruent(color):\n", " possible_incongruent_colors = [stimulus for stimulus in stimuli if stimulus != color]\n", " incongruent_color = random.choice(possible_incongruent_colors)\n", " return incongruent_color" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input", "remove-output" ] }, "outputs": [], "source": [ "#update how the text and color are set for the key stimulus\n", "cur_word = random.choice(stimuli) #notice the change in variable name now that we have congruent and incongruent trials\n", "trial_type = random.choice(trial_types) #we're just going to randomly pick the trial type (so it's 50/50 congruent/incongruent)\n", "\n", "word_stim.setText(cur_word) #set text\n", "if trial_type == 'incongruent':\n", " cur_color = make_incongruent(cur_word)\n", "else:\n", " cur_color = cur_word\n", "#notice that at this point cur_color is the color we're gonna set the word to. \n", "#It's taking into account the trial type \n", "word_stim.setColor(cur_color) #set color" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "8. **CHECKPOINT**\n", "\n", "The full code should now look like the code below." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input", "remove-output" ] }, "outputs": [], "source": [ "import time\n", "import sys\n", "import os\n", "import random\n", "from psychopy import visual,event,core,gui\n", "\n", "stimuli = ['red', 'orange', 'yellow', 'green', 'blue']\n", "valid_response_keys = ['r', 'o', 'y', 'g', 'b','q']\n", "trial_types = ['congruent','incongruent']\n", "\n", "def make_incongruent(color):\n", " possible_incongruent_colors = [stimulus for stimulus in stimuli if stimulus != color]\n", " incongruent_color = random.choice(possible_incongruent_colors)\n", " return incongruent_color\n", "\n", "win = visual.Window([800,600],color=\"gray\", units='pix',checkTiming=False)\n", "placeholder = visual.Rect(win,width=180,height=80, fillColor=\"lightgray\",lineColor=\"black\", lineWidth=6,pos=[0,0])\n", "word_stim = visual.TextStim(win,text=\"\", height=40, color=\"black\",pos=[0,0])\n", "instruction = visual.TextStim(win,text=\"Press the first letter of the ink color\", height=20, color=\"black\",pos=[0,-200],autoDraw=True)\n", "#add fixation cross\n", "fixation = visual.TextStim(win,height=40,color=\"black\",text=\"+\")\n", "# add a new feedback TextStim before the while loop\n", "feedback_incorrect = visual.TextStim(win,text=\"INCORRECT\", height=40, color=\"black\",pos=[0,0])\n", "feedback_too_slow = visual.TextStim(win,text=\"TOO SLOW\", height=40, color=\"black\",pos=[0,0])\n", "\n", "RTs=[] #set RT list\n", "response_timer = core.Clock() # set response timer clock\n", "key_pressed=False #need to initialize this for later\n", "while True:\n", "\n", " cur_word = random.choice(stimuli) #notice the change in variable name now that we have congruent and incongruent trials\n", " trial_type = random.choice(trial_types) #we're just going to randomly pick the trial type (so it's 50/50 congruent/incongruent)\n", "\n", " word_stim.setText(cur_word) #set text\n", " if trial_type == 'incongruent':\n", " cur_color = make_incongruent(cur_word)\n", " else:\n", " cur_color = cur_word\n", " #notice that at this point cur_color is the color we're gonna set the word to. \n", " #It's taking into account the trial type \n", " word_stim.setColor(cur_color) #set color\n", "\n", " #show fixation\n", " placeholder.draw()\n", " fixation.draw()\n", " win.flip()\n", " core.wait(.5)\n", "\n", " #short inter stimulus interval\n", " placeholder.draw()\n", " win.flip()\n", " core.wait(.5)\n", "\n", " #draw word stimulus\n", " placeholder.draw()\n", " word_stim.draw()\n", " win.flip()\n", "\n", " #get response\n", " response_timer.reset() # immediately after win.flip(), reset clock to measure RT\n", " key_pressed = event.waitKeys(keyList=valid_response_keys,maxWait=2) # maximum wait time of 2 s\n", " RTs.append(round(response_timer.getTime()*1000,0)) #add an RT to the list, rounded to the nearest millisecond\n", "\n", " # add feedback\n", " # if key_pressed is still FALSE/ no response was registered, present too slow feedback\n", " if not key_pressed:\n", " feedback_too_slow.draw()\n", " win.flip()\n", " core.wait(1)\n", " elif key_pressed[0] == cur_color[0]:\n", " #correct response\n", " pass\n", " elif key_pressed[0] == 'q':\n", " break\n", " else:\n", " feedback_incorrect.draw()\n", " win.flip()\n", " core.wait(1)\n", "\n", "print(RTs)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## PART 2: EXTEND THE BASIC STROOP TASK INTO A FULL EXPERIMENT\n", "\n", "In Part 2, we'll be extending the Stroop effect task you just coded into a more complete experiment that (1) generates stimulus lists that are then read in by the main script and (2) accepts runtime variables to assign participants to conditions and record participant codes.\n", "\n", "We'll use the all same stimuli, but introduce several variations: \n", "\n", "a. We'll introduce an orientation manipulation so that on 50% of trials the word is presented upside down (what effect do you think this will have on response times?)\n", "\n", "b. The output will now be written to a results file.\n", "\n", "We'll also start modularizing the code so that one part of it is responsible for generating the trials, another for reading in the trial list, another for showing the stimuli, and another for writing the participant's responses to a file." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "9. **Create a generate trials file**\n", "Complete the `generate_trials()` function below so that it writes a file with all the trials to be presented to the participant." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def generate_trials(subj_code, seed,num_repetitions=25):\n", " '''\n", " Writes a file named {subj_code_}trials.csv, one line per trial. Creates a trials subdirectory if one does not exist\n", " subj_code: a string corresponding to a participant's unique subject code\n", " seed: an integer specifying the random seed\n", " num_repetitions: integer specifying total times that combinations of trial type (congruent vs. incongruent) and orientation (upright vs. upside_down) should repeat (total number of trials = 4 * num_repetitions)\n", " '''\n", " import os\n", " import random\n", "\n", " # define general parameters and functions here\n", " \n", " # create a trials folder if it doesn't already exist\n", " try:\n", " os.mkdir('trials')\n", " except FileExistsError:\n", " print('Trials directory exists; proceeding to open file')\n", " f= open(f\"trials/{subj_code}_trials.csv\",\"w\")\n", "\n", " #write header\n", " header = separator.join([\"subj_code\",\"seed\",\"word\", 'color','trial_type','orientation'])\n", " f.write(header+'\\n')\n", " \n", " # write code to loop through creating and adding trials to the file here\n", "\n", "\n", " #close the file\n", " f.close()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ ":::{tip}\n", "The text at the start of the function explaining what it does and its arguments is called a \"docstring\". This is the proper way to document what a function does. Unlike comments, which are completely ignored by the Python interpreter, docstrings are part of the function and can be accessed like so: `print(generateTrials.__doc__)`. \n", ":::\n", "\n", "The produced file should be a CSV and have the following format:\n", "\n", "Col 1: subject code\n", "\n", "Col 2: the word to be shown\n", "\n", "Col 3: the color of the word\n", "\n", "Col 4: whether the trial is 'congruent' or 'incongruent'. 50% of the trials should be congruent and 50% incongruent (half/half).\n", "\n", "Col 5: The orientation of the word, \"upright\", or \"upside_down\". Please ensure that 50% of the congruent trials are upright (and the remaining are upside-down). Same thing for incongruent trials -- 50% should be upright and the remainder upside-down.\n", "\n", "Make sure the trial order is **randomized** (i.e., shuffle the order) so that you don't have all the congruent trials together, all the trials containg the same word together, etc." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "remove-output", "hide-input" ] }, "outputs": [], "source": [ "def generate_trials(subj_code, seed, num_repetitions = 25):\n", " '''\n", " Writes a file named {subj_code_}trials.csv, one line per trial. Creates a trials subdirectory if one does not exist\n", " subj_code: a string corresponding to a participant's unique subject code\n", " seed: an integer specifying the random seed\n", " num_repetitions: integer (or string) specifying total times that combinations of trial type (congruent vs. incongruent) and orientation (upright vs. upside_down) should repeat (total number of trials = 4 * num_repetitions)\n", " '''\n", " import os\n", " import random\n", "\n", " # define general parameters and functions here\n", " separator = \",\"\n", " colors = ['red', 'orange', 'yellow', 'green', 'blue']\n", " trial_types = [\"congruent\",\"incongruent\"]\n", " orientations = [\"upright\",\"upside_down\"]\n", " num_repetitions = int(num_repetitions)\n", "\n", " #set seed\n", " random.seed(int(seed))\n", "\n", " #define a function to make colors incongurent\n", " def make_incongruent(color, stimuli):\n", " possible_incongruent_colors = [stimulus for stimulus in stimuli if stimulus != color]\n", " incongruent_color = random.choice(possible_incongruent_colors)\n", " return incongruent_color\n", " \n", " # create a trials folder if it doesn't already exist\n", " try:\n", " os.mkdir('trials')\n", " except FileExistsError:\n", " print('Trials directory exists; proceeding to open file')\n", " f= open(f\"trials/{subj_code}_trials.csv\",\"w\")\n", "\n", " #write header\n", " header = separator.join([\"subj_code\",\"seed\",\"word\", 'color','trial_type','orientation'])\n", " f.write(header+'\\n')\n", " \n", " # write code to loop through creating trials here\n", " trial_data = []\n", " for i in range(num_repetitions):\n", " for cur_trial_type in trial_types:\n", " for cur_orientation in orientations:\n", " cur_word = random.choice(colors)\n", " if cur_trial_type == 'incongruent':\n", " cur_color = make_incongruent(cur_word,colors)\n", " else:\n", " cur_color = cur_word\n", " trial_data.append([subj_code, seed, cur_word, cur_color, cur_trial_type, cur_orientation])\n", " \n", " #shuffle the list\n", " random.shuffle(trial_data)\n", "\n", " #write the trials to the trials file\n", " for cur_trial in trial_data:\n", " f.write(separator.join(map(str,cur_trial))+'\\n')\n", "\n", " #close the file\n", " f.close()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "10. **Runtime variables**\n", "\n", "Extend the script you wrote for Part 1 to accept these 2 runtime variables [using a GUI box](https://cogs219.github.io/notebooks/Psychopy_reference.html#a-simple-function-for-popping-up-an-error-using-a-gui-window):\n", "\n", "- subject code (any string, but conventionally something like stroop_101, stroop_102, etc.)\n", "- seed (any integer)\n", "- number of repetitions (any integer)\n", "\n", "The entered values should be stored in a dictionary called `runtime_vars`. After the values are collected, the dictionary might look like this:\n", "\n", "```python\n", "runtime_vars= {'subj_code':'stroop_101','seed': 101, 'num_reps': 25}\n", "```\n", "\n", "```{note}\n", "This dictionary must be populated dynamically. You should not be hard-coding any of these values\n", "``` " ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input", "remove-output" ] }, "outputs": [], "source": [ "#add the following code to the main stroop task\n", "\n", "#function for collecting runtime variables\n", "def get_runtime_vars(vars_to_get,order,exp_version=\"Stroop\"):\n", " #Get run time variables, see http://www.psychopy.org/api/gui.html for explanation\n", " infoDlg = gui.DlgFromDict(dictionary=vars_to_get, title=exp_version, order=order)\n", " if infoDlg.OK:\n", " return vars_to_get\n", " else: \n", " print('User Cancelled')\n", "\n", "# get the runtime variables\n", "order = ['subj_code','seed','num_reps']\n", "runtime_vars = get_runtime_vars({'subj_code':'stroop_101','seed': 101, 'num_reps': 25}, order)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "11. **Read in a trials file**\n", "\n", "Extend the script further to read in the trials data-file. Once its read in, step through the file, one trial (i.e., line) at a time & show the appropriate stimuli. For example, if trial 3 in the trial file shows the word \"green\" in red font in an upside-down orientation, then that's what your experiments should be doing on trial 3. **Make sure to also now set the stimulus to the appropriate orientation on each trial!**" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input", "remove-output" ] }, "outputs": [], "source": [ "#import the generate files trial at the top of the main script\n", "from generate_trials import generate_trials" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "remove-output", "hide-input" ] }, "outputs": [], "source": [ "#add the import_trials function we've used in previous assignments\n", "def import_trials (trial_filename, col_names=None, separator=','):\n", " trial_file = open(trial_filename, 'r')\n", " \n", " if col_names is None:\n", " # Assume the first row contains the column names\n", " col_names = trial_file.readline().rstrip().split(separator)\n", " trials_list = []\n", " for cur_trial in trial_file:\n", " cur_trial = cur_trial.rstrip().split(separator)\n", " assert len(cur_trial) == len(col_names) # make sure the number of column names = number of columns\n", " trial_dict = dict(zip(col_names, cur_trial))\n", " trials_list.append(trial_dict)\n", " return trials_list" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "remove-output", "hide-input" ] }, "outputs": [], "source": [ "# generate a trial list\n", "generate_trials(runtime_vars['subj_code'],runtime_vars['seed'],runtime_vars['num_reps'])\n", "\n", "#read in trials\n", "trial_path = os.path.join(os.getcwd(),'trials',runtime_vars['subj_code']+'_trials.csv')\n", "trial_list = import_trials(trial_path)\n", "print(trial_list)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "remove-output", "hide-input" ] }, "outputs": [], "source": [ "#new trial loop\n", "for cur_trial in trial_list:\n", "\n", " cur_word = cur_trial['word']\n", " cur_color = cur_trial['color']\n", " trial_type = cur_trial['trial_type']\n", " cur_ori = cur_trial['orientation']\n", "\n", " word_stim.setText(cur_word) #set text\n", " word_stim.setColor(cur_color) #set color\n", "\n", " if cur_ori=='upside_down':\n", " word_stim.setOri(180)\n", " else:\n", " word_stim.setOri(0)\n", "\n", " #show fixation\n", " placeholder.draw()\n", " fixation.draw()\n", " win.flip()\n", " core.wait(.5)\n", "\n", " #short inter stimulus interval\n", " placeholder.draw()\n", " win.flip()\n", " core.wait(.5)\n", "\n", " #draw word stimulus\n", " placeholder.draw()\n", " word_stim.draw()\n", " win.flip()\n", "\n", " #get response\n", " response_timer.reset() # immediately after win.flip(), reset clock to measure RT\n", " key_pressed = event.waitKeys(keyList=valid_response_keys,maxWait=2) # maximum wait time of 2 s\n", " rt = round(response_timer.getTime()*1000,0)\n", "\n", " # add feedback\n", " # if key_pressed is still FALSE/ no response was registered, present too slow feedback\n", " if not key_pressed:\n", " feedback_too_slow.draw()\n", " win.flip()\n", " core.wait(1)\n", " elif key_pressed[0] == cur_color[0]:\n", " #correct response\n", " pass\n", " elif key_pressed[0] == 'q':\n", " break\n", " else:\n", " feedback_incorrect.draw()\n", " win.flip()\n", " core.wait(1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "12. **Write the data**\n", "\n", "Now, open a data file writes all the trial information **and** responses to a file after every response. \n", "\n", "You should be writing to the file after every trial (not at the very end). Make sure your data is written to `data/subject-code_data.csv` where `subject-code` is the subject code entered at runtime and `data/` is a subdirectory that will contain all the data files. Your code should record one line per response and record responses as they come in. If a user quits after trial 50, the data file should have recorded those 50 responses. Here's an example of what your output file should look like (note that the first line contains column name).\n", "\n", "| subj_code | seed | word | color | trial_type | orientation |trial_num | response | is_correct | rt |\n", "|------------|-----------|--------|-------|-------------|-------------|----------|----------|------------|-----|\n", "| stroop_101 | 101 | green | blue | incongruent | upright |1 | b | 1 | 898 |\n", "| stroop_101 | 101 | green | green | congruent | upright |2 | g | 1 | 754 |\n", "| stroop_101 | 101 | yellow | red | incongruent | upside_down |3 | y | 0 | 902 |\n", "\n", "```{note}\n", "Use a CSV format as before: fields are separated by commas\n", "``` \n", "\n", "```{admonition} Challenge!\n", "Ensure that you can't run the same participant code twice. If you enter a participant code that's already been entered before, PsychoPy should pop-up a warning box saying \"Participant code already exists\". This should prevent you from overwriting an existing data-file.\n", "```" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "remove-output", "hide-input" ] }, "outputs": [], "source": [ "#open data file and write header\n", "try:\n", " os.mkdir('data')\n", " print('Data directory did not exist. Created data/')\n", "except FileExistsError:\n", " pass \n", "separator=\",\"\n", "data_file = open(os.path.join(os.getcwd(),'data',runtime_vars['subj_code']+'_data.csv'),'w')\n", "header = separator.join(['subj_code','seed', 'word','color','trial_type','orientation','trial_num','response','is_correct','rt'])\n", "data_file.write(header+'\\n')\n", "\n", "response_timer = core.Clock() # set response timer clock\n", "# trial loop\n", "# add a trial number\n", "trial_num = 1\n", "for cur_trial in trial_list:\n", "\n", " cur_word = cur_trial['word']\n", " cur_color = cur_trial['color']\n", " trial_type = cur_trial['trial_type']\n", " cur_ori = cur_trial['orientation']\n", "\n", " word_stim.setText(cur_word) #set text\n", " word_stim.setColor(cur_color) #set color\n", "\n", " if cur_ori=='upside_down':\n", " word_stim.setOri(180)\n", " else:\n", " word_stim.setOri(0)\n", "\n", " #show fixation\n", " placeholder.draw()\n", " fixation.draw()\n", " win.flip()\n", " core.wait(.5)\n", "\n", " #short inter stimulus interval\n", " placeholder.draw()\n", " win.flip()\n", " core.wait(.5)\n", "\n", " #draw word stimulus\n", " placeholder.draw()\n", " word_stim.draw()\n", " win.flip()\n", "\n", " #get response\n", " response_timer.reset() # immediately after win.flip(), reset clock to measure RT\n", " key_pressed = event.waitKeys(keyList=valid_response_keys,maxWait=2) # maximum wait time of 2 s\n", " rt = round(response_timer.getTime()*1000,0)\n", "\n", " # add feedback\n", " # if key_pressed is still FALSE/ no response was registered, present too slow feedback\n", " if not key_pressed:\n", " is_correct = 0\n", " response = \"NA\"\n", " feedback_too_slow.draw()\n", " win.flip()\n", " core.wait(1)\n", " elif key_pressed[0] == cur_color[0]: \n", " is_correct = 1\n", " response = key_pressed[0]\n", " #correct response\n", " pass\n", " else:\n", " is_correct = 0\n", " response = key_pressed[0]\n", " feedback_incorrect.draw()\n", " win.flip()\n", " core.wait(1)\n", " \n", " #writing a response\n", " response_list=[cur_trial[_] for _ in cur_trial]\n", " print(response_list)\n", "\t#write dependent variables\n", " response_list.extend([trial_num,response,is_correct,rt])\n", " responses = map(str,response_list)\n", " print(response_list)\n", " line = separator.join([str(i) for i in response_list])\n", " data_file.write(line+'\\n')\n", "\n", " # increment trial number\n", " trial_num += 1\n", "\n", "#close the data file at the end of the experiment\n", "data_file.close()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "13. **FINAL MAIN CODE**\n", "\n", "The full main experiment file can should now look something like the code below." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "remove-output", "hide-input" ] }, "outputs": [], "source": [ "import time\n", "import sys\n", "import os\n", "import random\n", "from psychopy import visual,event,core,gui\n", "from generate_trials import generate_trials\n", "\n", "stimuli = ['red', 'orange', 'yellow', 'green', 'blue']\n", "valid_response_keys = ['r', 'o', 'y', 'g', 'b']\n", "trial_types = ['congruent','incongruent']\n", "\n", "#function for collecting runtime variables\n", "def get_runtime_vars(vars_to_get,order,exp_version=\"Stroop\"):\n", " #Get run time variables, see http://www.psychopy.org/api/gui.html for explanation\n", " infoDlg = gui.DlgFromDict(dictionary=vars_to_get, title=exp_version, order=order)\n", " if infoDlg.OK:\n", " return vars_to_get\n", " else: \n", " print('User Cancelled')\n", "\n", "#function for reading in trials\n", "def import_trials(trial_filename, col_names=None, separator=','):\n", " trial_file = open(trial_filename, 'r')\n", " \n", " if col_names is None:\n", " # Assume the first row contains the column names\n", " col_names = trial_file.readline().rstrip().split(separator)\n", " trials_list = []\n", " for cur_trial in trial_file:\n", " cur_trial = cur_trial.rstrip().split(separator)\n", " assert len(cur_trial) == len(col_names) # make sure the number of column names = number of columns\n", " trial_dict = dict(zip(col_names, cur_trial))\n", " trials_list.append(trial_dict)\n", " return trials_list\n", "\n", "win = visual.Window([800,600],color=\"gray\", units='pix',checkTiming=False)\n", "placeholder = visual.Rect(win,width=180,height=80, fillColor=\"lightgray\",lineColor=\"black\", lineWidth=6,pos=[0,0])\n", "word_stim = visual.TextStim(win,text=\"\", height=40, color=\"black\",pos=[0,0])\n", "instruction = visual.TextStim(win,text=\"Press the first letter of the ink color\", height=20, color=\"black\",pos=[0,-200],autoDraw=True)\n", "#add fixation cross\n", "fixation = visual.TextStim(win,height=40,color=\"black\",text=\"+\")\n", "# add a new feedback TextStim before the while loop\n", "feedback_incorrect = visual.TextStim(win,text=\"INCORRECT\", height=40, color=\"black\",pos=[0,0])\n", "feedback_too_slow = visual.TextStim(win,text=\"TOO SLOW\", height=40, color=\"black\",pos=[0,0])\n", "\n", "# get the runtime variables\n", "order = ['subj_code','seed','num_reps']\n", "runtime_vars = get_runtime_vars({'subj_code':'stroop_101','seed': 101, 'num_reps': 25}, order)\n", "\n", "# generate a trial list\n", "generate_trials(runtime_vars['subj_code'],runtime_vars['seed'],runtime_vars['num_reps'])\n", "\n", "#read in trials\n", "trial_path = os.path.join(os.getcwd(),'trials',runtime_vars['subj_code']+'_trials.csv')\n", "trial_list = import_trials(trial_path)\n", "print(trial_list)\n", "\n", "#open data file and write header\n", "try:\n", " os.mkdir('data')\n", " print('Data directory did not exist. Created data/')\n", "except FileExistsError:\n", " pass \n", "separator=\",\"\n", "data_file = open(os.path.join(os.getcwd(),'data',runtime_vars['subj_code']+'_data.csv'),'w')\n", "header = separator.join(['subj_code','seed', 'word','color','trial_type','orientation','trial_num','response','is_correct','rt'])\n", "data_file.write(header+'\\n')\n", "\n", "response_timer = core.Clock() # set response timer clock\n", "# trial loop\n", "# add a trial number\n", "trial_num = 1\n", "for cur_trial in trial_list:\n", "\n", " cur_word = cur_trial['word']\n", " cur_color = cur_trial['color']\n", " trial_type = cur_trial['trial_type']\n", " cur_ori = cur_trial['orientation']\n", "\n", " word_stim.setText(cur_word) #set text\n", " word_stim.setColor(cur_color) #set color\n", "\n", " if cur_ori=='upside_down':\n", " word_stim.setOri(180)\n", " else:\n", " word_stim.setOri(0)\n", "\n", " #show fixation\n", " placeholder.draw()\n", " fixation.draw()\n", " win.flip()\n", " core.wait(.5)\n", "\n", " #short inter stimulus interval\n", " placeholder.draw()\n", " win.flip()\n", " core.wait(.5)\n", "\n", " #draw word stimulus\n", " placeholder.draw()\n", " word_stim.draw()\n", " win.flip()\n", "\n", " #get response\n", " response_timer.reset() # immediately after win.flip(), reset clock to measure RT\n", " key_pressed = event.waitKeys(keyList=valid_response_keys,maxWait=2) # maximum wait time of 2 s\n", " rt = round(response_timer.getTime()*1000,0)\n", "\n", " # add feedback\n", " # if key_pressed is still FALSE/ no response was registered, present too slow feedback\n", " if not key_pressed:\n", " is_correct = 0\n", " response = \"NA\"\n", " feedback_too_slow.draw()\n", " win.flip()\n", " core.wait(1)\n", " elif key_pressed[0] == cur_color[0]: \n", " is_correct = 1\n", " response = key_pressed[0]\n", " #correct response\n", " pass\n", " else:\n", " is_correct = 0\n", " response = key_pressed[0]\n", " feedback_incorrect.draw()\n", " win.flip()\n", " core.wait(1)\n", " \n", " #writing a response\n", " response_list=[cur_trial[_] for _ in cur_trial]\n", " print(response_list)\n", "\t#write dependent variables\n", " response_list.extend([trial_num,response,is_correct,rt])\n", " responses = map(str,response_list)\n", " print(response_list)\n", " line = separator.join([str(i) for i in response_list])\n", " data_file.write(line+'\\n')\n", "\n", " # increment trial number\n", " trial_num += 1\n", "\n", "#close the data file at the end of the experiment\n", "data_file.close()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Final main code after completing the challenge:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input", "remove-output" ] }, "outputs": [], "source": [ "import time\n", "import sys\n", "import os\n", "import random\n", "from psychopy import visual,event,core,gui\n", "from generate_trials import generate_trials\n", "\n", "stimuli = ['red', 'orange', 'yellow', 'green', 'blue']\n", "valid_response_keys = ['r', 'o', 'y', 'g', 'b']\n", "trial_types = ['congruent','incongruent']\n", "\n", "def popupError(text):\n", " errorDlg = gui.Dlg(title=\"Error\", pos=(200,400))\n", " errorDlg.addText('Error: '+text, color='Red')\n", " errorDlg.show()\n", "\n", "def open_data_file(filename):\n", " \"\"\"\n", " Open data file, creating data/ directory as necesasry\n", " \"\"\"\n", " if os.path.isfile(filename):\n", " popupError(f'Error {filename} already exists')\n", " return False\n", " else:\n", " try:\n", " data_file = open(filename,'w')\n", " except FileNotFoundError:\n", " print(f'Could not open {filename} for writing')\n", " return data_file\n", "\n", "#function for collecting runtime variables\n", "#now throws an error if the data file already exists\n", "def get_runtime_vars(vars_to_get,order,exp_version=\"Exercise 3\"):\n", " \"\"\"\n", " Get run time variables, see http://www.psychopy.org/api/gui.html for explanation\n", " Return filled in runtime variables and an opened data file\n", " \"\"\"\n", " while True:\n", " infoDlg = gui.DlgFromDict(dictionary=vars_to_get, title=exp_version, order=order,copyDict=True) \n", " populated_runtime_vars = infoDlg.dictionary \n", " data_file = open_data_file(os.path.join(os.getcwd(),'data',populated_runtime_vars['subj_code']+'_data.csv'))\n", " if 'Choose' in list(populated_runtime_vars.values()):\n", " popupError('Need to choose a value from each dropdown box')\n", " elif infoDlg.OK and data_file:\n", " return populated_runtime_vars\n", " elif not infoDlg.OK:\n", " print('User Cancelled')\n", " sys.exit()\n", "\n", "#function for reading in trials\n", "def import_trials(trial_filename, col_names=None, separator=','):\n", " trial_file = open(trial_filename, 'r')\n", " \n", " if col_names is None:\n", " # Assume the first row contains the column names\n", " col_names = trial_file.readline().rstrip().split(separator)\n", " trials_list = []\n", " for cur_trial in trial_file:\n", " cur_trial = cur_trial.rstrip().split(separator)\n", " assert len(cur_trial) == len(col_names) # make sure the number of column names = number of columns\n", " trial_dict = dict(zip(col_names, cur_trial))\n", " trials_list.append(trial_dict)\n", " return trials_list\n", "\n", "win = visual.Window([800,600],color=\"gray\", units='pix',checkTiming=False)\n", "placeholder = visual.Rect(win,width=180,height=80, fillColor=\"lightgray\",lineColor=\"black\", lineWidth=6,pos=[0,0])\n", "word_stim = visual.TextStim(win,text=\"\", height=40, color=\"black\",pos=[0,0])\n", "instruction = visual.TextStim(win,text=\"Press the first letter of the ink color\", height=20, color=\"black\",pos=[0,-200],autoDraw=True)\n", "#add fixation cross\n", "fixation = visual.TextStim(win,height=40,color=\"black\",text=\"+\")\n", "# add a new feedback TextStim before the while loop\n", "feedback_incorrect = visual.TextStim(win,text=\"INCORRECT\", height=40, color=\"black\",pos=[0,0])\n", "feedback_too_slow = visual.TextStim(win,text=\"TOO SLOW\", height=40, color=\"black\",pos=[0,0])\n", "\n", "# get the runtime variables\n", "order = ['subj_code','seed','num_reps']\n", "runtime_vars = get_runtime_vars({'subj_code':'stroop_101','seed': 101, 'num_reps': 25}, order)\n", "print(runtime_vars)\n", "\n", "# generate a trial list\n", "generate_trials(runtime_vars['subj_code'],runtime_vars['seed'],runtime_vars['num_reps'])\n", "\n", "#read in trials\n", "trial_path = os.path.join(os.getcwd(),'trials',runtime_vars['subj_code']+'_trials.csv')\n", "trial_list = import_trials(trial_path)\n", "print(trial_list)\n", "\n", "#open data file and write header\n", "try:\n", " os.mkdir('data')\n", " print('Data directory did not exist. Created data/')\n", "except FileExistsError:\n", " pass \n", "separator=\",\"\n", "data_file = open(os.path.join(os.getcwd(),'data',runtime_vars['subj_code']+'_data.csv'),'w')\n", "header = separator.join(['subj_code','seed', 'word','color','trial_type','orientation','trial_num','response','is_correct','rt'])\n", "data_file.write(header+'\\n')\n", "\n", "response_timer = core.Clock() # set response timer clock\n", "# trial loop\n", "# add a trial number\n", "trial_num = 1\n", "for cur_trial in trial_list:\n", "\n", " cur_word = cur_trial['word']\n", " cur_color = cur_trial['color']\n", " trial_type = cur_trial['trial_type']\n", " cur_ori = cur_trial['orientation']\n", "\n", " word_stim.setText(cur_word) #set text\n", " word_stim.setColor(cur_color) #set color\n", "\n", " if cur_ori=='upside_down':\n", " word_stim.setOri(180)\n", " else:\n", " word_stim.setOri(0)\n", "\n", " #show fixation\n", " placeholder.draw()\n", " fixation.draw()\n", " win.flip()\n", " core.wait(.5)\n", "\n", " #short inter stimulus interval\n", " placeholder.draw()\n", " win.flip()\n", " core.wait(.5)\n", "\n", " #draw word stimulus\n", " placeholder.draw()\n", " word_stim.draw()\n", " win.flip()\n", "\n", " #get response\n", " response_timer.reset() # immediately after win.flip(), reset clock to measure RT\n", " key_pressed = event.waitKeys(keyList=valid_response_keys,maxWait=2) # maximum wait time of 2 s\n", " rt = round(response_timer.getTime()*1000,0)\n", "\n", " # add feedback\n", " # if key_pressed is still FALSE/ no response was registered, present too slow feedback\n", " if not key_pressed:\n", " is_correct = 0\n", " response = \"NA\"\n", " feedback_too_slow.draw()\n", " win.flip()\n", " core.wait(1)\n", " elif key_pressed[0] == cur_color[0]: \n", " is_correct = 1\n", " response = key_pressed[0]\n", " #correct response\n", " pass\n", " else:\n", " is_correct = 0\n", " response = key_pressed[0]\n", " feedback_incorrect.draw()\n", " win.flip()\n", " core.wait(1)\n", " \n", " #writing a response\n", " response_list=[cur_trial[_] for _ in cur_trial]\n", " print(response_list)\n", "\t#write dependent variables\n", " response_list.extend([trial_num,response,is_correct,rt])\n", " responses = map(str,response_list)\n", " print(response_list)\n", " line = separator.join([str(i) for i in response_list])\n", " data_file.write(line+'\\n')\n", "\n", " # increment trial number\n", " trial_num += 1\n", "\n", "#close the data file at the end of the experiment\n", "data_file.close()" ] } ], "metadata": { "celltoolbar": "Edit Metadata", "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.13" }, "toc": { "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": false, "toc_cell": true, "toc_position": { "height": "526px", "left": "0px", "right": "922px", "top": "111px", "width": "358px" }, "toc_section_display": "block", "toc_window_display": true }, "vscode": { "interpreter": { "hash": "57beecaf6908bae4f97de5e2dc8e8d0311fae5bc989593c172c307d13e31f6e4" } } }, "nbformat": 4, "nbformat_minor": 2 }