Pyff

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.