Example Feedback
This section contains a complete listing of the TestD2 Feedback, a Feedback implementing a computer version of the classic d2 test of attention, developed by Brickenkamp (Brickenkamp, R., Test d2, Verlag für Psychologie, Hogrefe, 1972) as paper-and-pencil test. The listing is complete and functionally identical with the TestD2 Feedback delivered with Pyff, however many blank lines, comments and the copyright statement in the beginning of the file where removed from this listing to make it shorter. The following subsections correspond to one or two methods of the TestD2 class. The complete listing of the TestD2 module is given at the end of this section.
In the paper-and-pencil version, the test consists of the letters d and p, which are printed on a sheet of paper in 14 lines with 47 letters per line. Each letter has between 1 and 4 vertical lines above or below. The subject's task is to cross out all occurrences of the letter d with two lines (target) on the current line as fast and correct as possible. A d with more or less than two lines or a p (non-targets) must not be crossed out. The subject usually has 20 seconds to process a line. After those 20 seconds the experimenter gives a signal and the subject has to process the next line. The number of mistakes (erroneously crossed out non d2s or not crossed out d2s) can be used afterwards to quantify the attention of the subject.
Our computerized version of TestD2 differs from the paper and pencil variant in the following ways: The stimuli are not presented in rows which have to be processed in a given short amount of time, but are presented one by one on the screen. For each stimulus the subject has to decide whether it is a target or a non-target by pressing the according key on the keyboard. Here is a series of screenhots of the running Feedback.
In the paper and pencil version, the subject has to process 14 rows of 47 symbols and has 20 seconds per row. In the standard configuration of our Feedback we present 14x20 symbols and give the user a maximum of 14 x (20 / 47) seconds. Of course this makes the results of our version not directly comparable with the paper and pencil variant, we think however that it still makes a good demonstration on how to implement a sophisticated feedback application with our framework.
Initialization of the Feedback
The TestD2 class is derived from the PygameFeedback Class. PygameFeedback takes care of proper initialization and shutdown of Pygame before and after the Feedback runs. It also provides helpful methods and members which make dealing with Pygame much easier.
import random
import pygame
from FeedbackBase.PygameFeedback import PygameFeedback
TARGETS = ['d11', 'd20', 'd02']
NON_TARGETS = ['d10', 'd01', 'd21', 'd12', 'd22',
'p10', 'p01', 'p11', 'p20', 'p02', 'p21', 'p12', 'p22']
class TestD2(PygameFeedback):
def init(self):
PygameFeedback.init(self)
self.caption = "Test D2"
self.random_seed = 1234
self.number_of_symbols = 47 * 14
self.seconds_per_symbol = 20 / 47.
self.targets_percent = 45.45
self.color = [0, 0, 0]
self.backgroundColor = [127, 127, 127]
self.fontheight = 200
self.key_target = "f"
self.key_nontarget = "j"
The init
method of TestD2 sets various variables controlling the behavior of
the Feedback. In the first line of init
the init
method of the parent class
is called. This is necessary since the parent's init
declares variables
needed to make PygameFeedback's methods work. caption
sets the caption text
of the Pygame window, random_seed
sets the seed for the random numbers
generator. This is important to make experiments reproducible when using random
numbers. number_of_symbols
, seconds_per_symbol
and targets_persent
set
how many symbols are presented at most, how much time the subject has to
process one symbol and the percentage of target symbols of all symbols
presented. The number of symbols and seconds per symbol are used to calculate
the duration of the experiment. Those three numbers are taken from the
standard Test D2 condition where a subject has 14 lines with 47 symbols per
line and 20 seconds time per line. color
, backgroundColor
and fontheight
control the look of the Feedback, color
is for the color of the symbols,
backgroundColor
for the color of the background and fontheight
the height
of the font in pixels. key_target
and key_nontarget
are the keys on the
keyboard the user has to click if s/he wants to mark a target or a non-target.
All variables declared in this method are immediately visible in the GUI after the Feedback was loaded. The experimenter can modify them as he likes and set them in the Feedback.
Pre- and Postmainloop
Pre- and postmainloop are invoked immediately before respectively after the
mainloop of the Feedback. Since the mainloop of a Feedback can be invoked
several times during the lifetime of a Feedback, variables which need to be
initialized before each run should be set in pre_mainloop
and evaluations of
the run should be done in post_mainloop
.
def pre_mainloop(self):
PygameFeedback.pre_mainloop(self)
self.generate_d2list()
self.generate_symbols()
self.current_index = 0
self.e1 = 0
self.e2 = 0
pygame.time.set_timer(pygame.QUIT, self.number_of_symbols * self.seconds_per_symbol * 1000)
self.clock.tick()
self.present_stimulus()
In pre_mainloop
we call the parent's pre_mainloop
which initializes Pygame.
Then we generate the sequence of stimuli, the graphics for the stimuli. The
variables current_index
represents the current position in the list of
stimuli, e1
and e2
are the number of errors of omission and errors of
commission. Then a QUIT
event is scheduled. This event will appear in
Pygame's event queue after the given time and marks the regular end of the
Feedback. Then the current time is measured and the first stimulus presented
and the mainloop starts.
def post_mainloop(self):
elapsed_seconds = self.clock.tick() / 1000.
PygameFeedback.post_mainloop(self)
tn = self.current_index + 1
error = self.e1 + self.e2
error_rate = 100. * error / tn
correctly_processed = tn - error
cp = correctly_processed - self.e2
rt_avg = elapsed_seconds / tn
print "Results:"
print "========"
print
print "Processed symbols: %i of %i" % (tn, self.number_of_symbols)
print "Elapsed time: %f sec" % elapsed_seconds
print "Correctly processed symbols: %i" % (correctly_processed)
print "Percentage of Errors: %f" % (error_rate)
print "Errors: %i" % error
print "... errors of omission: %i" % self.e1
print "... errors of commission: %i" % self.e2
print "Concentration Performance: %i" % cp
print "Average reaction time: %f sec" % rt_avg
In post_mainloop
we measure the time needed to run the experiment by
calling clock.tick()
a second time which returns the time in
milliseconds since the last call. Then we call the parent's
post_mainloop
which cleanly shuts down Pygame. After that various
results of the experiment are calculated and printed out.
Tick
def tick(self):
self.wait_for_pygame_event()
if self.keypressed:
key = self.lastkey_unicode
self.keypressed = False
if key not in (self.key_target, self.key_nontarget):
return
else:
if key == self.key_nontarget \
and self.d2list[self.current_index] in TARGETS:
self.e1 += 1
elif key == self.key_target \
and self.d2list[self.current_index] in NON_TARGETS:
self.e2 += 1
else:
pass
self.current_index += 1
if self.current_index > self.number_of_symbols - 1:
self.on_stop()
else:
self.present_stimulus()
The tick
method is called repeatedly during the
mainloop by the MainloopFeedback which is a parent Feedback of the
PygameFeedback. Usually the tick
method of PygameFeedback limits the
Framerate by calling self.clock.tick(self.FPS)
and processes Pygame's
event queue. This is the desired behaviour in most Pygame Feedbacks sporting a
mainloop. In this case, however the Feedback is not paced by small timesteps,
but by the keypresses of the subject. So we overwrite the parent's tick
to omit the call of self.clock.tick
and call
self.wait_for_pygame_event
instead of self.process_pygame_events
.
This means tick waits at this call until the user pressed a key (or an other
Pygame event appeared in the event queue). Then the key is checked if it is one
of the two available keys, and if the correct key was pressed. If not, the
corresponding error accumulator is increased by one. Then the position in the
list of stimuli is increased by one. If the index reached the end of the list,
the Feedback is stopped, otherwise the next stimulus is presented:
def present_stimulus(self):
self.screen.fill(self.backgroundColor)
symbol = self.d2list[self.current_index]
self.screen.blit(self.symbol[symbol],
self.symbol[symbol].get_rect(center=self.screen.get_rect().center))
pygame.display.flip()
Generating the D2 List
def generate_d2list(self):
random.seed(self.random_seed)
targets = int(round(self.number_of_symbols * self.targets_percent / 100))
non_targets = int(self.number_of_symbols - targets)
l = [random.choice(TARGETS) for i in range(targets)] + \
[random.choice(NON_TARGETS) for i in range(non_targets)]
random.shuffle(l)
for i in range(len(l) - 1):
if l[i] == l[i + 1]:
pool = TARGETS if l[i] in TARGETS else NON_TARGETS
new = random.choice(pool)
while new == l[i + 1]:
new = random.choice(pool)
l[i] = new
self.d2list = l
The method generate_d2list
sets the seed
for the random numbers generator to make the results reproducible and generates
the list of targets and non-targets. It uses the targets_percent
attribute to obey the correct ratio of targets and non-targets in the list. The
method also ensures that a symbol never appears twice or more in a row.
Generating the Symbols
def generate_symbols(self):
linewidth = self.fontheight / 11
font = pygame.font.Font(None, self.fontheight)
surface_d = font.render("d", True, self.color)
surface_p = font.render("p", True, self.color)
width, height = surface_d.get_size()
surface_l1 = pygame.Surface((width, height), pygame.SRCALPHA)
surface_l2 = pygame.Surface((width, height), pygame.SRCALPHA)
pygame.draw.line(surface_l1, self.color,
(width / 2, height / 10),
(width / 2, height - height / 10), linewidth)
pygame.draw.line(surface_l2, self.color,
(width / 3, height / 10),
(width / 3, height - height / 10), linewidth)
pygame.draw.line(surface_l2, self.color,
(2 * width / 3, height / 10),
(2 * width / 3, height - height / 10), linewidth)
self.symbol = {}
for symbol in TARGETS + NON_TARGETS:
surface = pygame.Surface((width, height * 3), pygame.SRCALPHA)
letter = surface_d if symbol[0] == 'd' else surface_p
surface.blit(letter, (0, height))
if symbol[1] == '1':
surface.blit(surface_l1, (0, 0))
elif symbol[1] == '2':
surface.blit(surface_l2, (0, 0))
if symbol[2] == '1':
surface.blit(surface_l1, (0, 2 * height))
elif symbol[2] == '2':
surface.blit(surface_l2, (0, 2 * height))
self.symbol[symbol] = surface
The method generate_symbols
generates
the images, or surfaces in Pygame lingo, for the various symbols and stores
them in an object attribute so the Feedback can later use them directly when
painting them on the screen. The Symbols are generated as a combination of a
letter and one of two possible lines above and below the letter. The method
first generates the images for the letters then the images for the lines and
glues them together in the last loop.
Main
if __name__ == "__main__":
fb = TestD2()
fb.on_init()
fb.on_play()
This is a useful idiom to start the feedback without the FeedbackController.