Psychophysics Study¶
In this example, we use SweetBean to generate an experimental sequence for a same-different Psychophysics experiment. In this experiment, each trials has the following sequence of events:
- a fixation cross is displayed for 1500 ms
- a set of two dot stimuli is displayed, one on the left and one on the right for 200ms. Each set contains a certain number of dots and the participant has to indicate whether the numbers of dots in the two sets are the same or different by pressing
y
orn
.
Installation¶
First, we will install SweetBean and Setup.
%%capture
!pip install git+https://github.com/AutoResearch/sweetbean
To illustrate the use of SweetBean, we first assume a fixed trial sequence:
timeline = [{'dots_left': 40, 'dots_right': 70},
{'dots_left': 70, 'dots_right': 70},
{'dots_left': 70, 'dots_right': 40},
{'dots_left': 70, 'dots_right': 70},
{'dots_left': 40, 'dots_right': 70},
{'dots_left': 70, 'dots_right': 40},
{'dots_left': 40, 'dots_right': 40},
{'dots_left': 40, 'dots_right': 40},
{'dots_left': 40, 'dots_right': 40},
{'dots_left': 70, 'dots_right': 40}]
Note: You can generate such a trial sequence automatically using SweetPea. For this particular use case, you may refer to this example tutorial.
Instruction Block¶
Many experiments require instructions that tell the participants what to do.
Creating instructions in SweetBean is quite simple. First, we define a number of text stimuli that the participant sees. Then, we specify the order of the text stimuli within a block of instructions.
Let's begin with writing down our instructions in html code. We can specify the key required to move on to the next instruction.
from sweetbean.stimulus import Text
introduction_welcome = Text(text='Welcome to our perception experiment.<br><br> \
Press the SPACE key to continue.',
choices=[' '])
introduction_pictures = Text(text='Each picture contains two sets of dots, one left and one right.<br><br>\
Press the SPACE key to continue.',
choices=[' '])
introduction_responses = Text(text='You have to indicate whether the two sets contain an equal number of dots.<br><br>\
Press the y-key for yes (equal number) and<br> the n-key for no (unequal number).<br><br>\
Press the SPACE key to continue.',
choices=[' '])
introduction_note = Text(text='Note: For each picture, you have only 2 seconds to respond, so respond quickly.<br><br>\
You can only respond with the y and n keys while the dots are shown.<br><br> \
Press the SPACE key to BEGIN the experiment.',
choices=[' '])
\ Press the SPACE key to continue.', choices=[' ']) introduction_pictures = Text(text='Each picture contains two sets of dots, one left and one right.
\ Press the SPACE key to continue.', choices=[' ']) introduction_responses = Text(text='You have to indicate whether the two sets contain an equal number of dots.
\ Press the y-key for yes (equal number) and
the n-key for no (unequal number).
\ Press the SPACE key to continue.', choices=[' ']) introduction_note = Text(text='Note: For each picture, you have only 2 seconds to respond, so respond quickly.
\ You can only respond with the y and n keys while the dots are shown.
\ Press the SPACE key to BEGIN the experiment.', choices=[' '])
Next, will pack these stimuli into a list to form an instruction block.
from sweetbean import Block
# create a list of instruction stimuli for the instruction block
introduction_list = [introduction_welcome,
introduction_pictures,
introduction_responses,
introduction_note]
# create the instruction block
instruction_block = Block(introduction_list)
Exit Block¶
Similarly, we can specify a final instruction displayed at the end of the experiment.
# create a text stimulus shown at the end of the experiment
instruction_exit = Text(duration=3000,
text='Thank you for participating in the experiment.',
)
# create a list of instruction stimuli for the exit block
exit_list = [instruction_exit]
# create the exit block
exit_block = Block(exit_list)
Task Block¶
Fixation Cross¶
First, we define the fixation cross. SweetBean provides a convenient method:
from sweetbean.stimulus import Fixation
duration = 1500 # the duration is given in ms
fixation = Fixation(duration)
Dots Stimulus¶
Next, we declare our sets of dots as features in the stimulus, using the timeline variables:
First,
declare the stimulus features dot_stimulus_left
and dot_stimulus_right
as timeline variables (since they come from the timeline). For the parser, we also need to provide all the possible levels of the stimulus. For now, we assume that each dot display can contain either 40 or 70 dots.
Then, we define the entire stimulus which is composed of the two features. SweetPea provides a convenient way of generating a stimulus with two sets of dots via RandomDotPatternsStimulus
.
from sweetbean.variable import TimelineVariable
from sweetbean.stimulus import RandomDotPatterns
# define the stimuli features as timeline variables
dot_stimulus_left = TimelineVariable('dots_left')
dot_stimulus_right = TimelineVariable('dots_right')
# We can use these variables in the stimuli declaration:
rdp = RandomDotPatterns(
duration=2000,
number_of_oobs=[dot_stimulus_left, dot_stimulus_right],
number_of_apertures=2,
choices=["y", "n"],
background_color="black",
)
Note that the dot stimulus is shown for 2000ms (duration=2000
). It consists of two set of dots (number_of_apertures=2
), which are parameterized by the two timeline variables number_of_oobs=[dot_stimulus_left, dot_stimulus_right]
. Finally, we allow participants to record a response on each stimulus, indicating whether the dots match or not by pressing the respective keys for y
and n
(choices=["y", "n"]
)
Task Event Sequence¶
Now, we define the event sequence which determines the order of events within a trial. SweetBean groups event into event sequences, and event sequences into blocks. Here, an event sequence corresponds to a trial and a block to series of trials.
from sweetbean import Block, Experiment
# define the sequence of events within a trial
event_sequence = [fixation, rdp]
# group trials into blocks
task_block = Block(event_sequence, timeline)
Experiment Block Sequence¶
Now that we have specified all of our experiment blocks, we put them together into an experiment. The function below compiles the experiment and converts it into a html file.
# define the entire experiment
experiment = Experiment([instruction_block, task_block, exit_block])
# export experiment to html file
experiment.to_html("psychophysics_experiment.html")
The code above should have generated a local html file rok_weber_fechner.html
which can be opened and run.
(Optional) Writing a Function to Automate the Generation of Stimulus Sequences¶
You can also integrate SweetBean into closed-loop behavioral research workflows, e.g., using AutoRA. This may involve calling a function that generates a novel jsPsych experiment from scratch, depending on the inputs.
The function below compiles the code above into a single function, and returns a web-based (JavaScript) experiment, written in jsPsych
.
The function takes a timeline, containing a sequence of trials, as input.
from sweetbean.stimulus import Text, Fixation, RandomDotPatterns
from sweetbean import Block, Experiment
from sweetbean.variable import TimelineVariable
def stimulus_sequence(timeline):
# INSTRUCTION BLOCK
# generate several text stimuli that serve as instructions
introduction_welcome = Text(text='Welcome to our perception experiment.<br><br> \
Press the SPACE key to continue.',
choices=[' '])
introduction_pictures = Text(text='Each picture contains two sets of dots, one left and one right.<br><br>\
Press the SPACE key to continue.',
choices=[' '])
introduction_responses = Text(text='You have to indicate whether the two sets contain an equal number of dots.<br><br>\
Press the y-key for yes (equal number) and<br> the n-key for no (unequal number).<br><br>\
Press the SPACE key to continue.',
choices=[' '])
introduction_note = Text(text='Note: For each picture, you have only 2 seconds to respond, so respond quickly.<br><br>\
You can only respond with the y and n keys while the dots are shown.<br><br> \
Press the SPACE key to BEGIN the experiment.',
choices=[' '])
# create a list of instruction stimuli for the instruction block
introduction_list = [introduction_welcome,
introduction_pictures,
introduction_responses,
introduction_note]
# create the instruction block
instruction_block = Block(introduction_list)
# EXIT BLOCK
# create a text stimulus shown at the end of the experiment
instruction_exit = Text(duration=3000,
text='Thank you for participating in the experiment.',
)
# create a list of instruction stimuli for the exit block
exit_list = [instruction_exit]
# create the exit block
exit_block = Block(exit_list)
# TASK BLOCK
# define fixation cross
fixation = Fixation(1500)
# define the stimuli features as timeline variables
dot_stimulus_left = TimelineVariable('dots_left')
dot_stimulus_right = TimelineVariable('dots_right')
# We can define a stimulus as a function of those stimulus features
rdp = RandomDotPatterns(
duration=2000,
number_of_oobs=[dot_stimulus_left, dot_stimulus_right],
number_of_apertures=2,
choices=["y", "n"],
background_color="black",
)
# define the sequence of events within a trial
event_sequence = [fixation, rdp]
# group trials into blocks
task_block = Block(event_sequence, timeline)
# EXPERIMENT
# define the entire experiment
experiment = Experiment([instruction_block, task_block, exit_block])
# return a js string to transfer to autora
return experiment.to_js_string(as_function=True, is_async=True)
\ Press the SPACE key to continue.', choices=[' ']) introduction_pictures = Text(text='Each picture contains two sets of dots, one left and one right.
\ Press the SPACE key to continue.', choices=[' ']) introduction_responses = Text(text='You have to indicate whether the two sets contain an equal number of dots.
\ Press the y-key for yes (equal number) and
the n-key for no (unequal number).
\ Press the SPACE key to continue.', choices=[' ']) introduction_note = Text(text='Note: For each picture, you have only 2 seconds to respond, so respond quickly.
\ You can only respond with the y and n keys while the dots are shown.
\ Press the SPACE key to BEGIN the experiment.', choices=[' ']) # create a list of instruction stimuli for the instruction block introduction_list = [introduction_welcome, introduction_pictures, introduction_responses, introduction_note] # create the instruction block instruction_block = Block(introduction_list) # EXIT BLOCK # create a text stimulus shown at the end of the experiment instruction_exit = Text(duration=3000, text='Thank you for participating in the experiment.', ) # create a list of instruction stimuli for the exit block exit_list = [instruction_exit] # create the exit block exit_block = Block(exit_list) # TASK BLOCK # define fixation cross fixation = Fixation(1500) # define the stimuli features as timeline variables dot_stimulus_left = TimelineVariable('dots_left') dot_stimulus_right = TimelineVariable('dots_right') # We can define a stimulus as a function of those stimulus features rdp = RandomDotPatterns( duration=2000, number_of_oobs=[dot_stimulus_left, dot_stimulus_right], number_of_apertures=2, choices=["y", "n"], background_color="black", ) # define the sequence of events within a trial event_sequence = [fixation, rdp] # group trials into blocks task_block = Block(event_sequence, timeline) # EXPERIMENT # define the entire experiment experiment = Experiment([instruction_block, task_block, exit_block]) # return a js string to transfer to autora return experiment.to_js_string(as_function=True, is_async=True)