# Part of the psychopy_ext library
# Copyright 2010-2014 Jonas Kubilius
# The program is distributed under the terms of the GNU General Public License,
# either version 3 of the License, or (at your option) any later version.
"""
A library of helper functions for creating and running experiments.
All experiment-related methods are kept here.
"""
import sys, os, csv, glob, random, warnings, copy
from UserDict import DictMixin
import numpy as np
import wx
# for HTML rendering
import pyglet
import textwrap
from HTMLParser import HTMLParser
# for exporting stimuli to svg
try:
    import svgwrite
except:
    no_svg = True
else:
    no_svg = False
import psychopy.info
from psychopy import visual, core, event, logging, misc, monitors, data
from psychopy.data import TrialHandler, ExperimentHandler
import ui
from version import __version__ as psychopy_ext_version
# pandas does not come by default with PsychoPy but that should not prevent
# people from running the experiment
try:
    import pandas
except:
    pass
[docs]class default_computer:
    """The default computer parameters. Hopefully will form a full class at
    some point.
    """
    recognized = False
    # computer defaults
    root = '.'  # means store output files here
    stereo = False  # not like in Psychopy; this merely creates two Windows
    default_keys = {'exit': ('lshift', 'escape'),
                    'trigger': 'space'}  # "special" keys
    valid_responses = {'f': 0, 'j': 1}  # organized as input value: output value
    # monitor defaults
    name = 'default'
    distance = 80
    width = 37.5
    # window defaults
    screen = 0  # default screen is 0
    view_scale = [1,1]
[docs]    def __init__(self):
        pass
  
[docs]def set_paths(exp_root='.', computer=default_computer, fmri_rel=''):
    """Set paths to data storage.
    :Args:
        exp_root (str)
            Path to where the main file that starts the program is.
    :Kwargs:
        - computer (Namespace, default: :class:`default_computer`)
            A class with a computer parameters defined, such as the default
            path for storing data, size of screen etc. See
            :class:`default_computer` for an example.
        - fmri_rel (str, default: '')
            A path to where fMRI data and related analyzes should be stored.
            This is useful because fMRI data takes a lot of space so you may
            want to keep it on an external hard drive rather than on Dropbox
            where your scripts might live, for example.
    :Returns:
        paths (dict):
            A dictionary of paths.
    """
    fmri_root = os.path.join(computer.root, fmri_rel)
    if exp_root != '':
        exp_root += '/'
    paths = {
        'root': computer.root,
        'exp_root': exp_root,
        'fmri_root': fmri_root,
        'analysis': os.path.join(exp_root, 'analysis/'),  # where analysis files are stored
        'logs': os.path.join(exp_root, 'logs/'),
        'data': os.path.join(exp_root, 'data/'),
        'report': 'report/',
        'data_behav': os.path.join(fmri_root, 'data_behav/'),  # for fMRI behav data
        'data_fmri': os.path.join(fmri_root,'data_fmri/'),
        'data_struct': os.path.join(fmri_root,'data_struct/'),  # anatomical data
        'spm_analysis': os.path.join(fmri_root, 'analysis/'),
        'rec': os.path.join(fmri_root,'reconstruction/'), # CARET reconstructions
        'rois': os.path.join(fmri_root,'rois/'),  # ROIs (no data, just masks)
        'data_rois': os.path.join(fmri_root,'data_rois/'), # preprocessed and masked data
        'sim': exp_root,  # path for storing simulations of models
        }
    return paths
 
def run_tests(computer):
    """Runs basic tests before starting the experiment.
    At the moment, it only checks if the computer is recognized and if not,
    it waits for a user confirmation to continue thus preventing from running
    an experiment with incorrect settings, such as stimuli size.
    :Kwargs:
        computer (Namespace)
            A class with a computer parameters defined, such as the default
            path for storing data, size of screen etc. See
            :class:`default_computer` for an example.
    """
    if not computer.recognized:
        resp = raw_input("WARNING: This computer is not recognized.\n"
                "To continue, simply hit Enter (default)\n"
                #"To memorize this computer and continue, enter 'm'\n"
                "To quit, enter 'q'\n"
                "Your choice [C,q]: ")
        while resp not in ['', 'c', 'q']:
            resp = raw_input("Choose between continue (c) and quit (q): ")
        if resp == 'q':
            sys.exit()
        #elif resp == 'm':
            #mac = uuid.getnode()
            #if os.path.isfile('computer.py'):
                #write_head = False
            #else:
                #write_head = True
            #try:
                #dataFile = open(datafile, 'ab')
            #print ("Computer %d is memorized. Remember to edit computer.py"
                   #"file to " % mac
class Task(TrialHandler):
[docs]    def __init__(self,
                 parent,
                 name='',
                 version='0.1',
                 method='random',
                 data_fname=None,
                 blockcol=None
                 ):
        """
        An extension of TrialHandler with many useful functions.
        :Args:
            parent (:class:`Experiment`)
                The Experiment to which this Tast belongs.
        :Kwargs:
            - name (str, default: '')
                Name of the task. Currently not used anywhere.
            - version (str, default: '0.1')
                Version of your experiment.
            - method ({'sequential', 'random'}, default: 'random')
                Order of trials:
                    - sequential: trials and blocks presented sequentially
                    - random: trials presented randomly, blocks sequentially
                    - fullRandom: converted to 'random'
                Note that there is no explicit possibility to randomize
                the order of blocks. This is intentional because you
                in fact define block order in the `blockcol`.
            - data_fname (str, default=None)
                The name of the main data file for storing output. If None,
                reuses :class:`~psychopy_ext.exp.Datafile` instance from
                its parent; otherwise, a new one is created
                (stored in ``self.datafile``).
            - blockcol (str, default: None)
                Column name in `self.exp_plan` that defines which trial
                should be presented during which block.
        """
        self.parent = parent
        self.computer = self.parent.computer
        self.paths = self.parent.paths
        self.name = name
        self.version = version
        self.nReps = 1  # fixed
        self.method = method
        if method == 'randomFull':
            self.method = 'random'
        if data_fname is None:
            self.datafile = parent.datafile
        else:
            self.datafile = Datafile(data_fname)
        self.blockcol = blockcol
        self.computer.valid_responses = parent.computer.valid_responses
        self._exit_key_no = 0
        self.blocks = []
        #self.info = parent.info
        #self.extraInfo = self.info  # just for compatibility with PsychoPy
        #self.rp = parent.rp
 
    def __str__(self, **kwargs):
        """string representation of the object"""
        return 'psychopy_ext.exp.Task'
[docs]    def quit(self, message=''):
        """What to do when exit is requested.
        """
        print  # in case there was anything without \n
        logging.warning(message)
        self.win.close()
        try:
            self.logfile.write('End time: %s\n' % data.getDateStr(format="%Y-%m-%d %H:%M"))
            self.logfile.write('end')
        except:  # no logfile
            pass
        core.quit()
 
[docs]    def setup_task(self):
        """
        Does all the dirty setup before running the experiment.
        Steps include:
            - Logging file setup (:func:`set_logging`)
            - Creating a :class:`~psychopy.visual.Window` (:func:`create_window`)
            - Creating stimuli (:func:`create_stimuli`)
            - Creating trial structure (:func:`create_trial`)
            - Combining trials into a trial list  (:func:`create_triaList`)
            - Creating a :class:`~psychopy.data.TrialHandler` using the
              defined trialList  (:func:`create_TrialHandler`)
        :Kwargs:
            create_win (bool, default: True)
                If False, a window is not created. This is useful when you have
                an experiment consisting of a couple of separate sessions. For
                the first one you create a window and want everything to be
                presented on that window without closing and reopening it
                between the sessions.
        """
        if not self.parent._initialized:
            raise Exception('You must first call Experiment.setup()')
        self.win = self.parent.win
        self.logfile = self.parent.logfile
        self.info = self.parent.info
        self.rp = self.parent.rp
        self.mouse = self.parent.mouse
        self.datafile.writeable = not self.rp['no_output']
        self._set_keys_flat()
        self.set_seed()
        self.create_stimuli()
        self.create_trial()
        if not hasattr(self, 'trial'):
            raise Exception('self.trial variable must be created '
                            'with the self.create_trial() method')
        # for backward compatibility: convert event dict into Event
        if isinstance(self.trial[0], dict):
            self.trial = [Event._fromdict(self, ev) for ev in self.trial]
        self.create_exp_plan()
        if not hasattr(self, 'exp_plan'):
            raise Exception('self.exp_plan variable must be created '
                            'with the self.create_exp_plan() method')
        ## convert Event.dur to a list of exp_plan length
        #for ev in self.trial:
            #if isinstance(ev.dur, (int, float)):
                #ev.dur = [ev.dur] * len(self.exp_plan)
        # determine if syncing to global time is necessary
        self.global_timing = True
        for ev in self.trial:
            # if event sits there waiting, global time does not apply
            if np.any(ev.dur == 0) or np.any(np.isinf(ev.dur)):
                self.global_timing = False
                break
        if self.rp['autorun']:
            # speed up the experiment
            for ev in self.trial:  # speed up each event
                #ev.dur = map(lambda x: float(x)/self.rp['autorun'], ev.dur)
                ev.dur /= self.rp['autorun']
            self.exp_plan = self.set_autorun(self.exp_plan)
        self.get_blocks()
 
    def _set_keys_flat(self):
        #if keylist is None:
        keylist = self.computer.default_keys.values()
        #else:
            #keylist.extend(self.computer.default_keys.values())
        keys = []
        for key in keylist:
            if isinstance(key, (tuple, list)):
                keys.append(key)
            else:
                keys.append([key])
        # keylist might have key combinations; get rid of them for now
        self.keylist_flat = []
        for key in keys:
            self.keylist_flat.extend(key)
    def set_seed(self):
        # re-initialize seed for each block of task
        # (if there is more than one task or more than one block)
        if len(self.parent.tasks) > 1 or len(self.blocks) > 1:
            self.seed = int(core.getAbsTime())  # generate a new seed
            date = data.getDateStr(format="%Y_%m_%d %H:%M (Year_Month_Day Hour:Min)")
            random.seed(self.seed)
            np.random.seed(self.seed)
            if not self.rp['no_output']:
                try:
                    message = 'Task %s: block %d' % (self.__str__, self.this_blockn+1)
                except:
                    message = 'Task %s' % self.__str__
                self.logfile.write('\n')
                self.logfile.write('#[ PsychoPy2 RuntimeInfoAppendStart ]#\n')
                self.logfile.write('  #[[ %s ]] #---------\n' % message)
                self.logfile.write('    taskRandomSeed.isSet: True\n')
                self.logfile.write('    taskRandomSeed.string: %d\n' % self.seed)
                self.logfile.write('    taskRunTime: %s\n' % date)
                self.logfile.write('    taskRunTime.epoch: %d\n' % self.seed)
                self.logfile.write('#[ PsychoPy2 RuntimeInfoappendEnd ]#\n')
                self.logfile.write('\n')
        else:
            self.seed = self.parent.seed
[docs]    def show_text(self, text='', wait=0, wait_stim=None, auto=0):
        """
        Presents an instructions screen.
        :Kwargs:
            - text (str, default: None)
                Text to show.
            - wait (float, default: 0)
                How long to wait after the end of showing instructions,
                in seconds.
            - wait_stim (stimulus or a list of stimuli, default: None)
                During this waiting, which stimuli should be shown.
                Usually, it would be a fixation spot.
            - auto (float, default: 0)
                Duration of time-out of the instructions screen,
                in seconds.
        """
        # for some graphics drivers (e.g., mine:)
        # draw() command needs to be invoked once
        # before it can draw properly
        visual.TextStim(self.win, text='').draw()
        self.win.flip()
        #instructions = visual.TextStim(self.win, text=text,
                                    #color='white', height=20, units='pix',
                                    #pos=(0, 0),  # don't know why
                                    #wrapWidth=40*20)
        text = textwrap.dedent(text)
        if text.find('\n') < 0:  # single line, no formatting
            html = '<h2><font face="sans-serif">%s</font></h2>' % text
            instr = visual.TextStim(self.win, units='pix')
            instr._pygletTextObj = pyglet.text.HTMLLabel(html)
            width = instr._pygletTextObj.content_width
            multiline = False
        else:
            try:
                import docutils.core
            except:  # will make plain formatting
                html = '<p><font face="sans-serif">%s</font></p>' % text
                html = html.replace('\n\n', '</font></p><p><font face="sans-serif">')
                html = html.replace('\n', '</font><br /><font face="sans-serif">')
                width = 40*12
                multiline = True
            else:
                html = docutils.core.publish_parts(text,
                                        writer_name='html')['html_body']
                html = _HTMLParser().feed(html)
                width = 40*12
                multiline = True
        instructions = visual.TextStim(self.win, units='pix',
                                       wrapWidth=width)
        instructions._pygletTextObj = pyglet.text.HTMLLabel(html,
                                  width=width, multiline=multiline,
                                  x=0, anchor_x='left', anchor_y='center')
        instructions.draw()
        self.win.flip()
        if self.rp['unittest']:
            print text
        if auto > 0:  # show text and blank out
            if self.rp['autorun']:
                auto = auto / self.rp['autorun']
            core.wait(auto)
        elif not self.rp['autorun'] or not self.rp['unittest']:
            this_key = None
            while this_key != self.computer.default_keys['trigger']:
                this_key = self.last_keypress()
                if len(this_key) > 0:
                    this_key = this_key.pop()
            if self.rp['autorun']:
                wait /= self.rp['autorun']
        self.win.flip()
        if wait_stim is not None:
            if not isinstance(wait_stim, (tuple, list)):
                wait_stim = [wait_stim]
            for stim in wait_stim:
                stim.draw()
            self.win.flip()
        core.wait(wait)  # wait a little bit before starting the experiment
        event.clearEvents()  # clear keys
 
[docs]    def create_fixation(self, shape='complex', color='black', size=.2):
        """Creates a fixation spot.
        :Kwargs:
            - shape: {'dot', 'complex'} (default: 'complex')
                Choose the type of fixation:
                    - dot: a simple fixation dot (.2 deg visual angle)
                    - complex: the 'best' fixation shape by `Thaler et al., 2012
                      <http://dx.doi.org/10.1016/j.visres.2012.10.012>`_ which
                      looks like a combination of s bulls eye and cross hair
                      (outer diameter: .6 deg, inner diameter: .2 deg). Note
                      that it is constructed by superimposing two rectangles on
                      a disk, so if non-uniform background will not be visible.
            - color (str, default: 'black')
                Fixation color.
        """
        if shape == 'complex':
            r1 = size  # radius of outer circle (degrees)
            r2 = size/3.  # radius of inner circle (degrees)
            edges = 8
            d = np.pi*2 / (4*edges)
            verts = [(r1*np.sin(e*d), r1*np.cos(e*d)) for e in xrange(edges+1)]
            verts.append([0,0])
            oval_pos = [(r2,r2), (r2,-r2), (-r2,-r2), (-r2,r2)]
            oval = []
            for i in range(4):
                oval.append(visual.ShapeStim(
                    self.win,
                    name = 'oval',
                    fillColor = color,
                    lineColor = None,
                    vertices = verts,
                    ori = 90*i,
                    pos = oval_pos[i]
                ))
            center = visual.Circle(
                self.win,
                name   = 'center',
                fillColor  = color,
                lineColor = None,
                radius   = r2,
            )
            fixation = GroupStim(stimuli=oval + [center],
                                 name='fixation')
            fixation.color = color
            self.fixation = fixation
        elif shape == 'dot':
            self.fixation = GroupStim(
                stimuli=visual.PatchStim(
                    self.win,
                    name   = 'fixation',
                    color  = 'red',
                    tex    = None,
                    mask   = 'circle',
                    size   = size,
                ),
                name='fixation')
 
[docs]    def create_stimuli(self):
        """
        Define stimuli as a dictionary
        Example::
            self.create_fixation(color='white')
            line1 = visual.Line(self.win, name='line1')
            line2 = visual.Line(self.win, fillColor='DarkRed')
            self.s = {
                'fix': self.fixation,
                'stim1': [visual.ImageStim(self.win, name='stim1')],
                'stim2': GroupStim(stimuli=[line1, line2], name='lines')
                }
        """
        raise NotImplementedError
 
[docs]    def create_trial(self):
        """
        Create a list of events that constitute a trial (``self.trial``).
        Example::
            self.trial = [exp.Event(self,
                                    dur=.100,
                                    display=self.s['fix'],
                                    func=self.idle_event),
                          exp.Event(self,
                                    dur=.300,
                                    display=self.s['stim1'],
                                    func=self.during_trial),
                           ]
        """
        raise NotImplementedError
 
[docs]    def create_exp_plan(self):
        """
        Put together trials into ``self.exp_plan``.
        Example::
            self.exp_plan = []
            for ...:
                exp_plan.append([
                    OrderedDict([
                        ('cond', cond),
                        ('name', names[cond]),
                        ('onset', ''),
                        ('dur', trial_dur),
                        ('corr_resp', corr_resp),
                        ('subj_resp', ''),
                        ('accuracy', ''),
                        ('rt', ''),
                        ])
                    ])
        """
        raise NotImplementedError
 
    def get_mouse_resp(self, keyList=None, timeStamped=False):
        """
        Returns mouse clicks.
        If ``self.respmap`` is provided, records clicks only when clicked
        inside respmap. This respmap is supposed to be a list of shape
        objects that determine boundaries of where one can click.
        Might change in the future if it gets incorporated in stimuli
        themselves.
        Note that mouse implementation is a bit shaky in PsychoPy at
        the moment. In particular, ``getPressed`` method returns
        multiple key down events per click. Thus, when calling
        ``get_mouse_resp`` from a while loo[, it is best to limit
        sampling to, for example, 150 ms (see `Jeremy's response <https://groups.google.com/d/msg/psychopy-users/HG4L-UDG93Y/FvyuB-OrsqoJ>`_).
        """
        mdict = {0: 'left-click', 1: 'middle-click', 2: 'right-click'}
        valid_mouse = [k for k,v in mdict.items() if v in self.computer.valid_responses]
        valid_mouse.sort()
        if timeStamped:
            mpresses, mtimes = self.mouse.getPressed(getTime=True)
        else:
            mpresses = self.mouse.getPressed(getTime=False)
        resplist = []
        if sum(mpresses) > 0:
            for but in valid_mouse:
                if mpresses[but] > 0:
                    if timeStamped:
                        resplist.append([mdict[but],mtimes[but]])
                    else:
                        resplist.append([mdict[but], None])
            if hasattr(self, 'respmap'):
                clicked = False
                for box in self.respmap:
                    if box.contains(self.mouse):
                        resplist = [tuple(r+[box]) for r in resplist]
                        clicked = True
                        break
                if not clicked:
                    resplist = []
        return resplist
    def get_resp(self, keyList=None, timeStamped=False):
        resplist = event.getKeys(keyList=keyList, timeStamped=timeStamped)
        if resplist is None:
            resplist = []
        mresp = self.get_mouse_resp(keyList=keyList, timeStamped=timeStamped)
        resplist += mresp
        return resplist
[docs]    def last_keypress(self, keyList=None, timeStamped=False):
        """
        Extract the last key pressed from the event list.
        If exit key is pressed (default: 'Left Shift + Esc'), quits.
        :Returns:
            A list of keys pressed.
        """
        if keyList is None:
            keyList = self.keylist_flat
        this_keylist = self.get_resp(keyList=keyList+self.keylist_flat,
                                     timeStamped=timeStamped)
        keys = []
        for this_key in this_keylist:
            isexit = self._check_if_exit(this_key)
            if not isexit:
                self._exit_key_no = 0
                isin_keylist = self._check_if_in_keylist(this_key, keyList)
                if isin_keylist:  # don't want to accept triggers and such
                    keys.append(this_key)
        return keys
 
    def _check_if_exit(self, this_key):
        """
        Checks if there one of the exit keys was pressed.
        :Args:
            this_key (str or tuple)
                Key or time-stamped key to check
        :Returns:
            True if any of the ``self.computer.default_keys['exit']``
            keys were pressed, False otherwise.
        """
        exit_keys = self.computer.default_keys['exit']
        if isinstance(this_key, tuple):
            this_key_exit = this_key[0]
        else:
            this_key_exit = this_key
        if this_key_exit in exit_keys:
            if self._exit_key_no < len(exit_keys):
                if exit_keys[self._exit_key_no] == this_key_exit:
                    if self._exit_key_no == len(exit_keys) - 1:
                        self.quit('Premature exit requested by user.')
                    else:
                        self._exit_key_no += 1
                else:
                    self._exit_key_no = 0
            else:
                self._exit_key_no = 0
        return self._exit_key_no > 0
    def _check_if_in_keylist(self, this_key, keyList):
        if isinstance(this_key, tuple):
            this_key_check = this_key[0]
        else:
            this_key_check = this_key
        return this_key_check in keyList
    def before_event(self):
        for stim in self.this_event.display:
            stim.draw()
        self.win.flip()
    def after_event(self):
        pass
[docs]    def wait_until_response(self, draw_stim=True):
        """
        Waits until a response key is pressed.
        Returns last key pressed, timestamped.
        :Kwargs:
            draw_stim (bool, default: True)
                Controls if stimuli should be drawn or have already
                been drawn (useful if you only want to redefine
                the drawing bit of this function).
        :Returns:
            A list of tuples with a key name (str) and a response time (float).
        """
        if draw_stim:
            self.before_event()
        event_keys = []
        event.clearEvents() # key presses might be stored from before
        while len(event_keys) == 0: # if the participant did not respond earlier
            if 'autort' in self.this_trial:
                if self.trial_clock.getTime() > self.this_trial['autort']:
                    event_keys = [(self.this_trial['autoresp'], self.this_trial['autort'])]
            else:
                event_keys = self.last_keypress(
                    keyList=self.computer.valid_responses.keys(),
                    timeStamped=self.trial_clock)
        return event_keys
 
[docs]    def idle_event(self, draw_stim=True):
        """
        Default idle function for an event.
        Sits idle catching default keys (exit and trigger).
        :Kwargs:
            draw_stim (bool, default: True)
                Controls if stimuli should be drawn or have already
                been drawn (useful if you only want to redefine
                the drawing bit of this function).
        :Returns:
            A list of tuples with a key name (str) and a response time (float).
        """
        if draw_stim:
            self.before_event()
        event_keys = None
        event.clearEvents() # key presses might be stored from before
        if self.this_event.dur == 0 or self.this_event.dur == np.inf:
            event_keys = self.last_keypress()
        else:
            event_keys = self.wait()
        return event_keys
 
[docs]    def feedback(self):
        """
        Gives feedback by changing fixation color.
        - Correct: fixation change to green
        - Wrong: fixation change to red
        """
        this_resp = self.all_keys[-1]
        if hasattr(self, 'respmap'):
            subj_resp = this_resp[2]
        else:
            subj_resp = self.computer.valid_responses[this_resp[0]]
        #subj_resp = this_resp[2]  #self.computer.valid_responses[this_resp[0]]
        # find which stimulus is fixation
        if isinstance(self.this_event.display, (list, tuple)):
            for stim in self.this_event.display:
                if stim.name in ['fixation', 'fix']:
                    fix = stim
                    break
        else:
            if self.this_event.display.name in ['fixation', 'fix']:
                fix = self.this_event.display
        if fix is not None:
            orig_color = fix.color  # store original color
            if self.this_trial['corr_resp'] == subj_resp:
                fix.setFillColor('DarkGreen')  # correct response
            else:
                fix.setFillColor('DarkRed')  # incorrect response
        for stim in self.this_event.display:
            stim.draw()
        self.win.flip()
        # sit idle
        self.wait()
        # reset fixation color
        fix.setFillColor(orig_color)
 
[docs]    def wait(self):
        """
        Wait until the event is over, register key presses.
        :Returns:
            A list of tuples with a key name (str) and a response time (float).
        """
        all_keys = []
        while self.check_continue():
            keys = self.last_keypress()
            if keys is not None:
                all_keys += keys
        return all_keys
 
[docs]    def check_continue(self):
        """
        Check if the event is not over yet.
        Uses ``event_clock``, ``trial_clock``, and, if
        ``self.global_timing`` is True, ``glob_clock`` to check whether
        the current event is not over yet. The event cannot last longer
        than event and trial durations and also fall out of sync from
        global clock.
        :Returns:
            A list of tuples with a key name (str) and a response time (float).
        """
        event_on = self.event_clock.getTime() < self.this_event.dur
        if self.global_timing:
            trial_on = self.trial_clock.getTime() < self.this_trial['dur']
            time_on = self.glob_clock.getTime() < self.cumtime + self.this_trial['dur']
        else:
            trial_on = True
            time_on = True
        return (event_on and trial_on and time_on)
 
[docs]    def set_autorun(self, exp_plan):
        """
        Automatically runs experiment by simulating key responses.
        This is just the absolute minimum for autorunning. Best practice would
        be extend this function to simulate responses according to your
        hypothesis.
        :Args:
            exp_plan (list of dict)
                A list of trial definitions.
        :Returns:
            exp_plan with ``autoresp`` and ``autort`` columns included.
        """
        def rt(mean):
            add = np.random.normal(mean,scale=.2)/self.rp['autorun']
            return self.trial[0].dur + add
        inverse_resp = invert_dict(self.computer.valid_responses)
        for trial in exp_plan:
            # here you could do if/else to assign different values to
            # different conditions according to your hypothesis
            trial['autoresp'] = random.choice(inverse_resp.values())
            trial['autort'] = rt(.5)
        return exp_plan
 
[docs]    def set_TrialHandler(self, trial_list, trialmap=None):
        """
        Converts a list of trials into a `~psychopy.data.TrialHandler`,
        finalizing the experimental setup procedure.
        """
        if len(self.blocks) > 1:
            self.set_seed()
        TrialHandler.__init__(self,
            trial_list,
            nReps=self.nReps,
            method=self.method,
            extraInfo=self.info,
            name=self.name,
            seed=self.seed)
        if trialmap is None:
            self.trialmap = range(len(trial_list))
        else:
            self.trialmap = trialmap
 
[docs]    def get_blocks(self):
        """
        Finds blocks in the given column of ``self.exp_plan``.
        The relevant column is stored in ``self.blockcol`` which is
        given by the user when initializing the experiment class.
        Produces a list of trial lists and trial mapping for each block.
        Trial mapping indicates where each trial is in the original
        `exp_plan` list.
        The output is stored in ``self.blocks``.
        """
        if self.blockcol is not None:
            blocknos = np.array([trial[self.blockcol] for trial in self.exp_plan])
            _, idx = np.unique(blocknos, return_index=True)
            blocknos = blocknos[np.sort(idx)].tolist()
            blocks = [None] * len(blocknos)
            for trialno, trial in enumerate(self.exp_plan):
                blockno = blocknos.index(trial[self.blockcol])
                if blocks[blockno] is None:
                    blocks[blockno] = [[trial], [trialno]]
                else:
                    blocks[blockno][0].append(trial)
                    blocks[blockno][1].append(trialno)
        else:
            blocks = [[self.exp_plan, range(len(self.exp_plan))]]
        self.blocks = blocks
 
[docs]    def before_task(self, text=None, wait=.5, wait_stim=None, **kwargs):
        """Shows text from docstring explaining the task.
        :Kwargs:
            - text (str, default: None)
                Text to show.
            - wait (float, default: .5)
                How long to wait after the end of showing instructions,
                in seconds.
            - wait_stim (stimulus or a list of stimuli, default: None)
                During this waiting, which stimuli should be shown.
                Usually, it would be a fixation spot.
            - \*\*kwargs
                Other parameters for :func:`~psychopy_ext.exp.Task.show_text()`
        """
        if len(self.parent.tasks) > 1:
            # if there are no blocks, try to show fixation
            if wait_stim is None:
                if len(self.blocks) <= 1:
                    try:
                        wait_stim = self.s['fix']
                    except:
                        wait = 0
                else:
                    wait = 0
            if text is None:
                self.show_text(text=self.__doc__, wait=wait,
                               wait_stim=wait_stim, **kwargs)
            else:
                self.show_text(text=text, wait=wait,
                               wait_stim=wait_stim, **kwargs)
 
[docs]    def run_task(self):
        """Sets up the task and runs it.
        If ``self.blockcol`` is defined, then runs block-by-block.
        """
        self.setup_task()
        self.before_task()
        self.datafile.open()
        for blockno, (block, trialmap) in enumerate(self.blocks):
            self.this_blockn = blockno
            # set TrialHandler only to the current block
            self.set_TrialHandler(block, trialmap=trialmap)
            self.run_block()
        self.datafile.close()
        self.after_task()
 
[docs]    def after_task(self, text=None, auto=1, **kwargs):
        """Useful for showing feedback after a task is done.
        For example, you could display accuracy.
        :Kwargs:
            - text (str, default: None)
                Text to show. If None, this is skipped.
            - auto (float, default: 1)
                Duration of time-out of the instructions screen,
                in seconds.
            - \*\*kwargs
                Other parameters for :func:`~psychopy_ext.exp.Task.show_text()`
        """
        if text is not None:
            self.show_text(text, auto=auto, **kwargs)
 
[docs]    def before_block(self, text=None, auto=1, wait=.5, wait_stim=None):
        """Show text before the block starts.
        Will not show anything if there's only one block.
        :Kwargs:
            - text (str, default: None)
                Text to show. If None, defaults to showing block number.
            - wait (float, default: .5)
                How long to wait after the end of showing instructions,
                in seconds.
            - wait_stim (stimulus or a list of stimuli, default: None)
                During this waiting, which stimuli should be shown.
                Usually, it would be a fixation spot. If None, this
                fixation spot will be attempted to be drawn.
            - auto (float, default: 1)
                Duration of time-out of the instructions screen,
                in seconds.
        """
        if len(self.blocks) > 1:
            if wait_stim is None:
                try:
                    wait_stim = self.s['fix']
                except:
                    pass
            if text is None:
                self.show_text(text='Block %d' % (self.this_blockn+1),
                               auto=auto, wait=wait, wait_stim=wait_stim)
            else:
                self.show_text(text=text, auto=auto, wait=wait, wait_stim=wait_stim)
 
[docs]    def run_block(self):
        """Run a block in a task.
        """
        self.before_block()
        # set up clocks
        self.glob_clock = core.Clock()
        self.trial_clock = core.Clock()
        self.event_clock = core.Clock()
        self.cumtime = 0
        # go over the trial sequence
        for this_trial in self:
            self.this_trial = this_trial
            self.run_trial()
        self.after_block()
 
[docs]    def after_block(self, text=None, **kwargs):
        """Show text at the end of a block.
        Will not show this text after the last block in the task.
        :Kwargs:
            - text (str, default: None)
                Text to show. If None, will default to
                'Pause. Hit ``trigger`` to continue.'
            - \*\*kwargs
                Other parameters for :func:`~psychopy_ext.exp.Task.show_text()`
        """
        # clear trial counting in the terminal
        sys.stdout.write('\r          ')
        sys.stdout.write('\r')
        sys.stdout.flush()
        if text is None:
            text = ('Pause. Hit %s to continue.' %
                                self.computer.default_keys['trigger'])
        # don't show this after the last block
        if self.this_blockn+1 < len(self.blocks):
            self.show_text(text=text, **kwargs)
 
    def before_trial(self):
        """What to do before trial -- nothing by default.
        """
        pass
[docs]    def run_trial(self):
        """Presents a trial.
        """
        self.before_trial()
        self.trial_clock.reset()
        self.this_trial['onset'] = self.glob_clock.getTime()
        sys.stdout.write('\rtrial %s' % (self.thisTrialN+1))
        sys.stdout.flush()
        self.this_trial['dur'] = 0
        for ev in self.trial:
            if ev.durcol is not None:
                ev.dur = self.this_trial[ev.durcol]
            self.this_trial['dur'] += ev.dur
        self.all_keys = []
        for event_no, this_event in enumerate(self.trial):
            self.this_event = this_event
            self.event_no = event_no
            self.run_event()
        # if autorun and responses were not set yet, get them now
        if len(self.all_keys) == 0 and self.rp['autorun'] > 0:
            self.all_keys += [(self.this_trial['autoresp'], self.this_trial['autort'])]
        self.post_trial()
        # correct timing if autorun
        if self.rp['autorun'] > 0:
            try:
                self.this_trial['autort'] *= self.rp['autorun']
                self.this_trial['rt'] *= self.rp['autorun']
            except:  # maybe not all keys are present
                pass
            self.this_trial['onset'] *= self.rp['autorun']
            self.this_trial['dur'] *= self.rp['autorun']
        self.datafile.write_header(self.info.keys() + self.this_trial.keys())
        self.datafile.write(self.info.values() + self.this_trial.values())
        self.cumtime += self.this_trial['dur']
        # update exp_plan with new values
        try:
            self.exp_plan[self.trialmap[self.thisIndex]] = self.this_trial
        except:  # for staircase
            self.exp_plan.append(self.this_trial)
 
[docs]    def post_trial(self):
        """A default function what to do after a trial is over.
        It records the participant's response as the last key pressed,
        calculates accuracy based on the expected (correct) response value,
        and records the time of the last key press with respect to the onset
        of a trial. If no key was pressed, participant's response and response
        time are recorded as an empty string, while accuracy is assigned a
        'No response'.
        :Args:
            - this_trial (dict)
                A dictionary of trial properties
            - all_keys (list of tuples)
                A list of tuples with the name of the pressed key and the time
                of the key press.
        :Returns:
            this_trial with ``subj_resp``, ``accuracy``, and ``rt`` filled in.
        """
        if len(self.all_keys) > 0:
            this_resp = self.all_keys.pop()
            if hasattr(self, 'respmap'):
                subj_resp = this_resp[2]
            else:
                subj_resp = self.computer.valid_responses[this_resp[0]]
            self.this_trial['subj_resp'] = subj_resp
            try:
                acc = signal_det(self.this_trial['corr_resp'], subj_resp)
            except:
                pass
            else:
                self.this_trial['accuracy'] = acc
                self.this_trial['rt'] = this_resp[1]
        else:
            self.this_trial['subj_resp'] = ''
            try:
                acc = signal_det(self.this_trial['corr_resp'], self.this_trial['subj_resp'])
            except:
                pass
            else:
                self.this_trial['accuracy'] = acc
                self.this_trial['rt'] = ''
 
[docs]    def run_event(self):
        """Presents a trial and catches key presses.
        """
        # go over each event in a trial
        self.event_clock.reset()
        self.mouse.clickReset()
        # show stimuli
        event_keys = self.this_event.func()
        if isinstance(event_keys, tuple):
            event_keys = [event_keys]
        elif event_keys is None:
            event_keys = []
        if len(event_keys) > 0:
            self.all_keys += event_keys
        # this is to get keys if we did not do that during trial
        self.all_keys += self.last_keypress(
            keyList=self.computer.valid_responses.keys(),
            timeStamped=self.trial_clock)
 
[docs]    def get_behav_df(self, pattern='%s'):
        """
        Extracts data from files for data analysis.
        :Kwargs:
            pattern (str, default: '%s')
                A string with formatter information. Usually it contains a path
                to where data is and a formatter such as '%s' to indicate where
                participant ID should be incorporated.
        :Returns:
            A `pandas.DataFrame` of data for the requested participants.
        """
        return get_behav_df(self.info['subjid'], pattern=pattern)
 
[docs]class SVG(object):
[docs]    def __init__(self, win, filename='image'):
        if no_svg:
            raise ImportError("Module 'svgwrite' not found.")
        #visual.helpers.setColor(win, win.color)
        win.contrast = 1
        self.win = win
        self.aspect = self.win.size[0]/float(self.win.size[1])
        self.open(filename)
 
    def open(self, filename):
        filename = filename.split('.svg')[0]
        self.svgfile = svgwrite.Drawing(profile='tiny',filename='%s.svg' % filename,
                                size=('%dpx' % self.win.size[0],
                                      '%dpx' % self.win.size[1]),
                                # set default units to px; from http://stackoverflow.com/a/13008664
                                viewBox=('%d %d %d %d' %
                                         (0,0,
                                          self.win.size[0],
                                          self.win.size[1]))
                                )
        bkgr = self.svgfile.rect(insert=(0,0), size=('100%','100%'),
                            fill=self.color2rgb255(self.win))
        self.svgfile.add(bkgr)
    def save(self):
        self.svgfile.save()
    def color2attr(self, stim, attr, color='black', colorSpace=None, kwargs = {}):
        col = self.color2rgb255(stim, color=color, colorSpace=colorSpace)
        if col is None:
            kwargs[attr + '_opacity'] = 0
        else:
            kwargs[attr] = col
            kwargs[attr + '_opacity'] = 1
        return kwargs
    def write(self, stim):
        if 'Circle' in str(stim):
            color_kw = self.color2attr(stim, 'stroke', color=stim.lineColor,
                                       colorSpace=stim.lineColorSpace)
            color_kw = self.color2attr(stim, 'fill', color=stim.fillColor,
                                       colorSpace=stim.fillColorSpace,
                                       kwargs=color_kw)
            svgstim = self.svgfile.circle(
                            center=self.get_pos(stim),
                            r=self.get_size(stim, stim.radius),
                            stroke_width=stim.lineWidth,
                            opacity=stim.opacity,
                            **color_kw
                            )
        elif 'ImageStim' in str(stim):
            raise NotImplemented
        elif 'Line' in str(stim):
            color_kw = self.color2attr(stim, 'stroke', color=stim.lineColor,
                                       colorSpace=stim.lineColorSpace)
            svgstim = self.svgfile.line(
                    start=self.get_pos(stim, stim.start),
                    end=self.get_pos(stim, stim.end),
                    stroke_width=stim.lineWidth,
                    opacity=stim.opacity,
                    **color_kw
                    )
        elif 'Polygon' in str(stim):
            raise NotImplemented
            #svgstim = self.svgfile.polygon(
                    #points=...,
                    #stroke_width=stim.lineWidth,
                    #stroke=self.color2rgb255(stim, color=stim.lineColor,
                                             #colorSpace=stim.lineColorSpace),
                    #fill=self.color2rgb255(stim, color=stim.fillColor,
                                           #colorSpace=stim.fillColorSpace)
                    #)
        elif 'Rect' in str(stim):
            color_kw = self.color2attr(stim, 'stroke', color=stim.lineColor,
                                       colorSpace=stim.lineColorSpace)
            color_kw = self.color2attr(stim, 'fill', color=stim.fillColor,
                                       colorSpace=stim.fillColorSpace,
                                       kwargs=color_kw)
            svgstim = self.svgfile.rect(
                    insert=self.get_pos(stim, offset=(-stim.width/2., -stim.height/2.)),
                    size=(self.get_size(stim, stim.width), self.get_size(stim, stim.height)),
                    stroke_width=stim.lineWidth,
                    opacity=stim.opacity,
                    **color_kw
                    )
        elif 'ThickShapeStim' in str(stim):
            svgstim = stim.to_svg(self)
        elif 'ShapeStim' in str(stim):
            points = self._calc_attr(stim, np.array(stim.vertices))
            points[:, 1] *= -1
            color_kw = self.color2attr(stim, 'stroke', color=stim.lineColor,
                                       colorSpace=stim.lineColorSpace)
            color_kw = self.color2attr(stim, 'fill', color=stim.fillColor,
                                       colorSpace=stim.fillColorSpace,
                                       kwargs=color_kw)
            if stim.closeShape:
                svgstim = self.svgfile.polygon(
                        points=points,
                        stroke_width=stim.lineWidth,
                        opacity=stim.opacity,
                        **color_kw
                        )
            else:
                svgstim = self.svgfile.polyline(
                        points=points,
                        stroke_width=stim.lineWidth,
                        opacity=stim.opacity,
                        **color_kw
                        )
            tr = self.get_pos(stim)
            svgstim.translate(tr[0], tr[1])
        elif 'SimpleImageStim' in str(stim):
            raise NotImplemented
        elif 'TextStim' in str(stim):
            if stim.fontname == '':
                font = 'arial'
            else:
                font = stim.fontname
            svgstim = self.svgfile.text(text=stim.text,
                                    insert=self.get_pos(stim) + np.array([0,stim.height/2.]),
                                    fill=self.color2rgb255(stim),
                                    font_family=font,
                                    font_size=stim.heightPix,
                                    text_anchor='middle',
                                    opacity=stim.opacity
                                    )
        else:
            svgstim = stim.to_svg(self)
        if not isinstance(svgstim, list):
            svgstim = [svgstim]
        for st in svgstim:
            self.svgfile.add(st)
    def get_pos(self, stim, pos=None, offset=None):
        if pos is None:
            pos = stim.pos
        if offset is not None:
            offset = self._calc_attr(stim, np.array(offset))
        else:
            offset = np.array([0,0])
        pos = self._calc_attr(stim, pos)
        pos = self.win.size/2 + np.array([pos[0], -pos[1]]) + offset
        return pos
    def get_size(self, stim, size=None):
        if size is None:
            size = stim.size
        size = self._calc_attr(stim, size)
        return size
    def _calc_attr(self, stim, attr):
        if stim.units == 'height':
            try:
                len(attr) == 2
            except:
                out = (attr * stim.win.size[1])
            else:
                out = (attr * stim.win.size * np.array([1./self.aspect, 1]))
        elif stim.units == 'norm':
            try:
                len(attr) == 2
            except:
                out = (attr * stim.win.size[1]/2)
            else:
                out = (attr * stim.win.size/2)
        elif stim.units == 'pix':
            out = attr
        elif stim.units == 'cm':
            out = misc.cm2pix(attr, stim.win.monitor)
        elif stim.units in ['deg', 'degs']:
            out = misc.deg2pix(attr, stim.win.monitor)
        else:
            raise NotImplementedError
        return out
    def color2rgb255(self, stim, color=None, colorSpace=None):
        """
        Convert color to RGB255 while adding contrast
        #Requires self.color, self.colorSpace and self.contrast
        Modified from psychopy.visual.BaseVisualStim._getDesiredRGB
        """
        if color is None:
            color = stim.color
        if isinstance(color, str) and stim.contrast == 1:
            color = color.lower()  # keep the nice name
        else:
            # Ensure that we work on 0-centered color (to make negative contrast values work)
            if colorSpace is None:
                colorSpace = stim.colorSpace
            if colorSpace not in ['rgb', 'dkl', 'lms', 'hsv']:
                color = (color / 255.0) * 2 - 1
            # Convert to RGB in range 0:1 and scaled for contrast
            # although the shader then has to convert it back it gets clamped en route otherwise
            try:
                color = (color * stim.contrast + 1) / 2.0 * 255
                color = 'rgb(%d,%d,%d)' % (color[0],color[1],color[2])
            except:
                color = None
        return color
 
[docs]class Datafile(object):
[docs]    def __init__(self, filename, writeable=True, header=None):
        """
        A convenience class for managing data files.
        Output is recorded in a comma-separeated (csv) file.
        .. note:: In the output file, floats are formatted to 1 ms precision so
                  that output files are nice.
        :Args:
            filename (str)
                Path to the file name
        :Kwargs:
            - writeable (bool, defualt: True)
                Can data be written in file or not. Might seem a bit silly
                but it is actually very useful because you can create
                a file and tell it to write data without thinking
                whether `no_output` is set.
            - header (list, default: None)
                If you give a header, then it will already be written
                in the datafile. Usually it's better to wait and write
                it only when the first data line is available.
        """
        self.filename = filename
        self.writeable = writeable
        self._header_written = False
        if header is not None:
            self.write_header(header)
        else:
            self.header = header
 
    def open(self):
        """Opens a csv file for writing data
        """
        if self.writeable:
            try_makedirs(os.path.dirname(self.filename))
            try:
                self.dfile = open(self.filename, 'ab')
                self.datawriter = csv.writer(self.dfile, lineterminator = '\n')
            except IOError:
                raise IOError('Cannot write to the data file %s!' % self.filename)
    def close(self):
        """Closes the file
        """
        if self.writeable:
            self.dfile.close()
    def write(self, data):
        """
        Writes data list to a file.
        .. note:: In the output file, floats are formatted to 1 ms precision so
                  that output files are nice.
        :Args:
            data (list)
                A list of values to write in a datafile
        """
        if self.writeable:
            # cut down floats to 1 ms precision
            dataf = ['%.3f'%i if isinstance(i,float) else i for i in data]
            self.datawriter.writerow(dataf)
    def write_header(self, header):
        """Determines if a header should be writen in a csv data file.
        Works by reading the first line and comparing it to the given header.
        If the header already is present, then a new one is not written.
        :Args:
            header (list of str)
                A list of column names
        """
        self.header = header
        if self.writeable and not self._header_written:
            write_head = False
            # no header needed if the file already exists and has one
            try:
                dataf_r = open(self.filename, 'rb')
                dataread = csv.reader(dataf_r)
            except:
                pass
            else:
                try:
                    header_file = dataread.next()
                except:  # empty file
                    write_head = True
                else:
                    if header == header_file:
                        write_head = False
                    else:
                        write_head = True
                dataf_r.close()
            if write_head:
                self.datawriter.writerow(header)
            self._header_written = True
 
class Experiment(ExperimentHandler, Task):
[docs]    def __init__(self,
                 name='',
                 version='0.1',
                 info=None,
                 rp=None,
                 actions=None,
                 computer=default_computer,
                 paths=None,
                 data_fname=None,
                 **kwargs
                 ):
        """
        An extension of ExperimentHandler and TrialHandler with many
        useful functions.
        .. note:: When you inherit this class, you must have at least
                 ``info`` and ``rp`` (or simply ``**kwargs``) keywords
                 because :class:`~psychopy.ui.Control` expects them.
        :Kwargs:
            - name (str, default: '')
                Name of the experiment. It will be used to call the
                experiment from the command-line.
            - version (str, default: '0.1')
                Version of your experiment.
            - info (tuple, list of tuples, or dict, default: None)
                Information about the experiment that you want to see in the
                output file. This is equivalent to PsychoPy's ``extraInfo``.
                It will contain at least ``('subjid', 'subj')`` even if a
                user did not specify that.
            - rp (tuple, list of tuples, or dict, default: None)
                Run parameters that apply for this particular run but need
                not be stored in the data output. It will contain at least
                the following::
                    [('no_output', False),  # do you want output? or just playing around?
                     ('debug', False),  # not fullscreen presentation etc
                     ('autorun', 0),  # if >0, will autorun at the specified speed
                     ('unittest', False),  # like autorun but no breaks at show_instructions
                     ('repository', ('do nothing', 'commit and push', 'only commit')),  # add, commit and push to a hg repo?
                                                                          # add and commit changes, like new data files?
                     ]
            - actions (list of function names, default: None)
                A list of function names (as ``str``) that can be called from
                GUI.
            - computer (module, default: ``default_computer``)
                Computer parameter module.
            - paths (dict, default: None)
                A dictionary of paths where to store different outputs.
                If None, :func:`~psychopy_ext.exp.set_paths()` is called.
            - data_fname (str, default=None)
                The name of the main data file for storing output. If None,
                becomes ``self.paths['data'] + self.info['subjid'] + '.csv'``.
                Then a :class:`~psychopy_ext.exp.Datafile` instance is
                created in ``self.datafile`` for easy writing to a csv
                format.
            - \*\*kwargs
        """
        ExperimentHandler.__init__(self,
            name=name,
            version=version,
            extraInfo=info,
            dataFileName='.data'  # for now so that PsychoPy doesn't complain
            )
        self.computer = computer
        if paths is None:
            self.paths = set_paths()
        else:
            self.paths = paths
        self._initialized = False
        # minimal parameters that Experiment expects in info and rp
        self.info = OrderedDict([('subjid', 'subj')])
        if info is not None:
            if isinstance(info, (list, tuple)):
                try:
                    info = OrderedDict(info)
                except:
                    info = OrderedDict([info])
            self.info.update(info)
        self.rp = OrderedDict([  # these control how the experiment is run
            ('no_output', False),  # do you want output? or just playing around?
            ('debug', False),  # not fullscreen presentation etc
            ('autorun', 0),  # if >0, will autorun at the specified speed
            ('unittest', False),  # like autorun but no breaks when instructions shown
            ('repository', ('do nothing', 'commit & push', 'only commit')),  # add, commit and push to a hg repo?
                                                                 # add and commit changes, like new data files?
            ])
        if rp is not None:
            if isinstance(rp, (tuple, list)):
                try:
                    rp = OrderedDict(rp)
                except:
                    rp = OrderedDict([rp])
            self.rp.update(rp)
        #if not self.rp['notests']:
            #run_tests(self.computer)
        self.actions = actions
        if data_fname is None:
            filename = self.paths['data'] + self.info['subjid'] + '.csv'
            self.datafile = Datafile(filename, writeable=not self.rp['no_output'])
        else:
            self.datafile = Datafile(data_fname, writeable=not self.rp['no_output'])
        if self.rp['unittest']:
            self.rp['autorun'] = 100
        self.tasks = []  # a list to store all tasks for this exp
        Task.__init__(self,
            self,
            #name=name,
            version=version,
            **kwargs
            )
 
    def __str__(self, **kwargs):
        """string representation of the object"""
        return 'psychopy_ext.exp.Experiment'
    #def add_tasks(self, tasks):
        #if isinstance(tasks, str):
            #tasks = [tasks]
        #for task in tasks:
            #task = task()
            #task.computer = self.computer
            #task.win = self.win
            #if task.info is not None:
                #task.info.update(self.info)
            #if task.rp is not None:
                #task.rp.update(self.rp)
            #self.tasks.append(task)
[docs]    def set_logging(self, logname='log.log', level=logging.WARNING):
        """Setup files for saving logging information.
        New folders might be created.
        :Kwargs:
            logname (str, default: 'log.log')
                The log file name.
        """
        if not self.rp['no_output']:
            # add .log if no extension given
            if not logname.endswith('.log'): logname += '.log'
            # Setup logging file
            try_makedirs(os.path.dirname(logname))
            if os.path.isfile(logname):
                writesys = False  # we already have sysinfo there
            else:
                writesys = True
            self.logfile = logging.LogFile(logname, filemode='a', level=level)
            # Write system information first
            if writesys:
                self.logfile.write('%s' % self.runtime_info)
            self.logfile.write('\n\n\n' + '#'*40 + '\n\n')
            self.logfile.write('$ python %s\n\n' % ' '.join(sys.argv))
            self.logfile.write('Start time: %s\n\n' % data.getDateStr(format="%Y-%m-%d %H:%M"))
        else:
            self.logfile = None
        # output to the screen
        logging.console.setLevel(level)
 
    def create_seed(self, seed=None):
        """
        SUPERSEDED by `psychopy.info.RunTimeInfo`
        Creates or assigns a seed for a reproducible randomization.
        When a seed is set, you can, for example, rerun the experiment with
        trials in exactly the same order as before.
        :Kwargs:
            seed (int, default: None)
                Pass a seed if you already have one.
        :Returns:
            self.seed (int)
        """
        if seed is None:
            try:
                self.seed = np.sum([ord(d) for d in self.info['date']])
            except:
                self.seed = 1
                logging.warning('No seed provided. Setting seed to 1.')
        else:
            self.seed = seed
        return self.seed
    def _guess_participant(self, data_path, default_subjid='01'):
        """Attempts to guess participant ID (it must be int).
        .. :Warning:: Not usable yet
        First lists all csv files in the data_path, then finds a maximum.
        Returns maximum+1 or an empty string if nothing is found.
        """
        datafiles = glob.glob(data_path+'*.csv')
        partids = []
        #import pdb; pdb.set_trace()
        for d in datafiles:
            filename = os.path.split(d)[1]  # remove the path
            filename = filename.split('.')[0]  # remove the extension
            partid = filename.split('_')[-1]  # take the numbers at the end
            try:
                partids.append(int(partid))
            except:
                logging.warning('Participant ID %s is invalid.' %partid)
        if len(partids) > 0: return '%02d' %(max(partids) + 1)
        else: return default_subjid
    def _guess_runno(self, data_path, default_runno = 1):
        """Attempts to guess run number.
        .. :Warning:: Not usable yet
        First lists all csv files in the data_path, then finds a maximum.
        Returns maximum+1 or an empty string if nothing is found.
        """
        if not os.path.isdir(data_path): runno = default_runno
        else:
            datafiles = glob.glob(data_path + '*.csv')
            # Splits file names into ['data', %number%, 'runType.csv']
            allnums = [int(os.path.basename(thisfile).split('_')[1]) for thisfile in datafiles]
            if allnums == []: # no data files yet
                runno = default_runno
            else:
                runno = max(allnums) + 1
                # print 'Guessing runNo: %d' %runNo
        return runno
    def get_mon_sizes(self, screen=None):
        warnings.warn('get_mon_sizes is deprecated; '
                      'use exp.get_mon_sizes instead')
        return get_mon_sizes(screen=screen)
[docs]    def create_win(self, debug=False, color='DimGray', units='deg',
                   winType='pyglet', **kwargs):
        """Generates a :class:`psychopy.visual.Window` for presenting stimuli.
        :Kwargs:
            - debug (bool, default: False)
                - If True, then the window is half the screen size.
                - If False, then the windon is full screen.
            - color (str, str with a hexadecimal value, or a tuple of 3 values, default: "DimGray')
                Window background color. Default is dark gray. (`See accepted
                color names <http://www.w3schools.com/html/html_colornames.asp>`_
        """
        current_level = logging.getLevel(logging.console.level)
        logging.console.setLevel(logging.ERROR)
        monitor = monitors.Monitor(self.computer.name,
            distance=self.computer.distance,
            width=self.computer.width)
        logging.console.setLevel(current_level)
        res = get_mon_sizes(self.computer.screen)
        monitor.setSizePix(res)
        if 'size' not in kwargs:
            try:
                kwargs['size'] = self.computer.win_size
            except:
                if not debug:
                    kwargs['size'] = tuple(res)
                else:
                    kwargs['size'] = (res[0]/2, res[1]/2)
        for key in kwargs:
            if key in ['monitor', 'fullscr', 'allowGUI', 'screen', 'viewScale']:
                del kwargs[key]
        self.win = visual.Window(
            monitor=monitor,
            units=units,
            fullscr=not debug,
            allowGUI=debug, # mouse will not be seen unless debugging
            color=color,
            winType=winType,
            screen=self.computer.screen,
            viewScale=self.computer.view_scale,
            **kwargs
        )
 
[docs]    def setup(self):
        """
        Initializes the experiment.
        A random seed is set for `random` and `numpy.random`. The seed
        is set using the 'set:time' option.
        Also, runtime information is fully recorded, log file is set
        and a window is created.
        """
        try:
            with open(sys.argv[0], 'r') as f: lines = f.read()
        except:
            author = 'None'
            version = 'None'
        else:
            author = None
            version = None
        #if not self.rp['no_output']:
        self.runtime_info = psychopy.info.RunTimeInfo(author=author,
                version=version, verbose=True, win=False, randomSeed='set:time')
        key, value = get_version()
        self.runtime_info[key] = value  # updates with psychopy_ext version
        self._set_keys_flat()
        self.seed = int(self.runtime_info['experimentRandomSeed.string'])
        np.random.seed(self.seed)
        #else:
            #self.runtime_info = None
            #self.seed = None
        self.set_logging(self.paths['logs'] + self.info['subjid'])
        self.create_win(debug=self.rp['debug'])
        self.mouse = event.Mouse(win=self.win)
        self._initialized = True
        #if len(self.tasks) == 0:
            ##self.setup = Task.setup
            #Task.setup(self)
 
[docs]    def before_exp(self, text=None, wait=.5, wait_stim=None, **kwargs):
        """
        Instructions at the beginning of the experiment.
        :Kwargs:
            - text (str, default: None)
                Text to show.
            - wait (float, default: .5)
                How long to wait after the end of showing instructions,
                in seconds.
            - wait_stim (stimulus or a list of stimuli, default: None)
                During this waiting, which stimuli should be shown.
                Usually, it would be a fixation spot.
            - \*\*kwargs
                Other parameters for :func:`~psychopy_ext.exp.Task.show_text()`
        """
        if wait_stim is None:
            if len(self.tasks) <= 1:
                try:
                    wait_stim = self.s['fix']
                except:
                    wait = 0
            else:
                wait = 0
        if text is None:
            self.show_text(text=self.__doc__, wait=wait,
                           wait_stim=wait_stim, **kwargs)
        else:
            self.show_text(text=text, wait=wait, wait_stim=wait_stim,
                           **kwargs)
 
[docs]    def run(self):
        """Alias to :func:`~psychopy_ext.exp.Experiment.run_exp()`
        """
        self.run_exp()
 
[docs]    def run_exp(self):
        """Sets everything up and calls tasks one by one.
        At the end, committing to a repository is possible. Use
        ``register`` and ``push`` flags (see
        :class:`~psychopy_ext.exp.Experiment` for more)
        """
        self.setup()
        self.before_exp()
        if len(self.tasks) == 0:
            self.run_task()
        else:
            for task in self.tasks:
                task(self).run_task()
        self.after_exp()
        self.repo_action()
        self.quit()
 
[docs]    def after_exp(self, text=None, auto=1, **kwargs):
        """Text after the experiment is over.
        :Kwargs:
            - text (str, default: None)
                Text to show. If None, defaults to
                'End of Experiment. Thank you!'
            - auto (float, default: 1)
                Duration of time-out of the instructions screen,
                in seconds.
            - \*\*kwargs
                Other parameters for :func:`~psychopy_ext.exp.Task.show_text()`
        """
        if text is None:
            self.show_text(text='End of Experiment. Thank you!',
                      auto=auto, **kwargs)
        else:
            self.show_text(text=text, auto=auto, **kwargs)
 
[docs]    def autorun(self):
        """
        Automatically runs the experiment just like it would normally
        work but automatically (as defined in
        :func:`~psychopy_ext.exp.set_autorun()`) and
        at the speed specified by `self.rp['autorun']` parameter. If
        speed is not specified, it is set to 100.
        """
        if not hasattr(self.rp, 'autorun'):
            self.rp['autorun'] = 100
        self.run()
 
    def repo_action(self):
        if isinstance(self.rp['repository'], tuple):
            self.rp['repository'] = self.rp['repository'][0]
        if self.rp['repository'] == 'commit & push':
            text = 'committing data and pushing to remote server...'
        elif self.rp['repository'] == 'only commit':
            text = 'commiting data...'
        if self.rp['repository'] != 'do nothing':
            textstim = visual.TextStim(self.win, text=text, height=.3)
            textstim.draw()
            timer = core.CountdownTimer(2)
            self.win.flip()
            if self.rp['repository'] == 'commit & push':
                self.commitpush()
            elif self.rp['repository'] == 'only commit':
                self.commit()
            while timer.getTime() > 0 and len(self.last_keypress()) == 0:
                pass
    def register(self, **kwargs):
        """Alias to :func:`~psychopy_ext.exp.commit()`
        """
        return self.commit(**kwargs)
[docs]    def commit(self, message=None):
        """
        Add and commit changes in a repository.
        TODO: How to set this up.
        """
        if message is None:
            message = 'data for participant %s' % self.info['subjid']
        cmd, out, err = ui._repo_action('commit', message=message)
        self.logfile.write('\n'.join([cmd, out, err]))
        return err
 
[docs]    def commitpush(self, message=None):
        """
        Add, commit, and push changes to a remote repository.
        Currently, only Mercurial repositories are supported.
        TODO: How to set this up.
        TODO: `git` support
        """
        err = self.commit(message=message)
        if err == '':
            out = ui._repo_action('push')
            self.logfile.write('\n'.join(out))
 
[docs]class Event(object):
[docs]    def __init__(self, parent, name='', dur=.300, durcol=None,
                 display=None, func=None):
        """
        Defines event displays.
        :Args:
            parent (:class:`~psychopy_ext.exp.Experiment` or
            :class:`~psychopy_ext.exp.Task`)
        :Kwargs:
            - name (str, default: '')
                Event name.
            - dur (int/float or a list of int/float, default: .300)
                Event duration (in seconds). If events have different
                durations throughout experiment, you can provide a list
                of durations which must be of the same length as the
                number of trials.
            - display (stimulus or a list of stimuli, default: None)
                Stimuli that are displayed during this event. If *None*,
                displays a fixation spot (or, if not created, creates
                one first).
            - func (function, default: None)
                Function to perform . If *None*, defaults to
                :func:`~psychopy_ext.exp.Task.idle_event`.
        """
        self.parent = parent
        self.name = name
        self.dur = dur  # will be converted to a list during setup
        self.durcol = durcol
        if display is None:
            try:
                self.display = parent.fixation
            except:
                parent.create_fixation()
                self.display = parent.fixation
        else:
            self.display = display
        if isinstance(self.display, tuple):
            self.display = list(self.display)
        elif not isinstance(self.display, list):
            self.display = [self.display]
        if func is None:
            self.func = parent.idle_event
        else:
            self.func = func
 
    @staticmethod
    def _fromdict(parent, entries):
        """
        Create an Event instance from a dictionary.
        This is only meant for backward compatibility and should not
        be used in general.
        """
        if 'defaultFun' in entries:
            entries['func'] = entries['defaultFun']
            del entries['defaultFun']
        return Event(parent, **entries)
        #self.__dict__.update(entries)
        #for key, value in dictionary.items():
            #self.key = value
 
[docs]class ThickShapeStim(visual.ShapeStim):
    """
    Draws thick shape stimuli as a collection of lines.
    PsychoPy has a bug in some configurations of not drawing lines thicker
    than 2px. This class fixes the issue. Note that it's really just a
    collection of rectanges so corners will not look nice.
    """
[docs]    def __init__(self,
                 win,
                 units  ='',
                 lineWidth=.01,
                 lineColor=(1.0,1.0,1.0),
                 lineColorSpace='rgb',
                 fillColor=None,
                 fillColorSpace='rgb',
                 vertices=((-0.5,0),(0,+0.5),(+0.5,0)),
                 closeShape=True,
                 pos= (0,0),
                 size=1,
                 ori=0.0,
                 opacity=1.0,
                 contrast=1.0,
                 depth  =0,
                 interpolate=True,
                 lineRGB=None,
                 fillRGB=None,
                 name='', autoLog=True):
        """
        :Parameters:
            lineWidth : int (or float?)
                specifying the line width in units of your choice
            vertices : a list of lists or a numpy array (Nx2)
                specifying xy positions of each vertex
            closeShape : True or False
                Do you want the last vertex to be automatically connected to the first?
            interpolate : True or False
                If True the edge of the line will be antialiased.
                """
        #what local vars are defined (these are the init params) for use by __repr__
        self._initParams = dir()
        self._initParams.remove('self')
        # Initialize inheritance and remove unwanted methods
        try:
            visual.BaseVisualStim.__init__(self, win, units=units, name=name, autoLog=False) #autoLog is set later
        except:  # PsychoPy prior to 1.79
            visual._BaseVisualStim.__init__(self, win, units=units, name=name, autoLog=False) #autoLog is set later
        self.__dict__['setColor'] = None
        self.__dict__['color'] = None
        self.__dict__['colorSpace'] = None
        self.contrast = float(contrast)
        self.opacity = float(opacity)
        self.pos = np.array(pos, float)
        self.closeShape=closeShape
        self.lineWidth=lineWidth
        self.interpolate=interpolate
        # Color stuff
        self.useShaders=False#since we don't ned to combine textures with colors
        self.__dict__['lineColorSpace'] = lineColorSpace
        self.__dict__['fillColorSpace'] = fillColorSpace
        if lineRGB!=None:
            logging.warning("Use of rgb arguments to stimuli are deprecated. Please use color and colorSpace args instead")
            self.setLineColor(lineRGB, colorSpace='rgb')
        else:
            self.setLineColor(lineColor, colorSpace=lineColorSpace)
        if fillRGB!=None:
            logging.warning("Use of rgb arguments to stimuli are deprecated. Please use color and colorSpace args instead")
            self.setFillColor(fillRGB, colorSpace='rgb')
        else:
            self.setFillColor(fillColor, colorSpace=fillColorSpace)
        # Other stuff
        self.depth=depth
        self.ori = np.array(ori,float)
        self.size = np.array([0.0,0.0])
        self.setSize(size, log=False)
        self.setVertices(vertices)
        #self._calcVerticesRendered()
        #set autoLog (now that params have been initialised)
        self.autoLog= autoLog
        if autoLog:
            logging.exp("Created %s = %s" %(self.name, str(self)))
 
    def draw(self):
        for stim in self.stimulus:
            stim.draw()
    def to_svg(self, svg):
        rects = []
        for stim, vertices in zip(self.stimulus,self.vertices):
            size = svg.get_size(stim, np.abs(stim.vertices[0])*2)
            points = svg._calc_attr(stim, np.array(vertices))
            points[:, 1] *= -1
            rect = svg.svgfile.polyline(
                    points=points,
                    stroke_width=svg._calc_attr(self,self.lineWidth),
                    stroke=svg.color2rgb255(self, color=self.lineColor,
                                colorSpace=self.lineColorSpace),
                    fill_opacity=0
                    )
            tr = svg.get_pos(self)#+size/2.
            rect.translate(tr[0], tr[1])
            rects.append(rect)
        return rects
    def setOri(self, newOri):
        # theta = (newOri - self.ori)/180.*np.pi
        # rot = np.array([[np.cos(theta), -np.sin(theta)],[np.sin(theta), np.cos(theta)]])
        # for stim in self.stimulus:
            # newVert = []
            # for vert in stim.vertices:
                # #import pdb; pdb.set_trace()
                # newVert.append(np.dot(rot,vert))
            # stim.setVertices(newVert)
        self.ori = newOri
        self.setVertices(self.vertices)
    def setPos(self, newPos):
        #for stim in self.stimulus:
            #stim.setPos(newPos)
        self.pos = newPos
        self.setVertices(self.vertices)
    #def setSize(self, newSize):
        ##for stim in self.stimulus:
            ##stim.setPos(newPos)
        #self.size = newSize
        #self.setVertices(self.vertices)
    def setVertices(self, value=None):
        if isinstance(value[0][0], int) or isinstance(value[0][0], float):
            self.vertices = [value]
        else:
            self.vertices = value
        self.stimulus = []
        theta = self.ori/180.*np.pi #(newOri - self.ori)/180.*np.pi
        rot = np.array([[np.cos(theta), -np.sin(theta)],[np.sin(theta), np.cos(theta)]])
        self._rend_vertices = []
        for vertices in self.vertices:
            rend_verts = []
            if self.closeShape:
                numPairs = len(vertices)
            else:
                numPairs = len(vertices)-1
            if self.units == 'pix':
                w = 1
            elif self.units in ['height', 'norm']:
                w = 1./self.win.size[1]
            elif self.units == 'cm':
                w = misc.pix2cm(1, self.win.monitor)
            elif self.units in ['deg', 'degs']:
                w = misc.pix2deg(1, self.win.monitor)
            wh = self.lineWidth/2. - w
            for i in range(numPairs):
                thisPair = np.array([vertices[i],vertices[(i+1)%len(vertices)]])
                thisPair_rot = np.dot(thisPair, rot.T)
                edges = [
                    thisPair_rot[1][0]-thisPair_rot[0][0],
                    thisPair_rot[1][1]-thisPair_rot[0][1]
                    ]
                lh = np.sqrt(edges[0]**2 + edges[1]**2)/2.
                rend_vert = [[-lh,-wh],[-lh,wh], [lh,wh],[lh,-wh]]
                #import pdb; pdb.set_trace()
                line = visual.ShapeStim(
                    self.win,
                    lineWidth   = 1,
                    lineColor   = self.lineColor,#None,
                    interpolate = True,
                    fillColor   = self.lineColor,
                    ori         = -np.arctan2(edges[1],edges[0])*180/np.pi,
                    pos         = np.mean(thisPair_rot,0) + self.pos,
                    # [(thisPair_rot[0][0]+thisPair_rot[1][0])/2. + self.pos[0],
                                   # (thisPair_rot[0][1]+thisPair_rot[1][1])/2. + self.pos[1]],
                    vertices    = rend_vert
                )
                #line.setOri(self.ori-np.arctan2(edges[1],edges[0])*180/np.pi)
                self.stimulus.append(line)
                rend_verts.append(rend_vert[0])
            rend_verts.append(rend_vert[1])
            self._rend_vertices.append(rend_verts)
            #import pdb; pdb.set_trace()
            #self.setSize(self.size)
 
[docs]class GroupStim(object):
    """
    A convenience class to put together stimuli in a single group.
    You can then do things like `stimgroup.draw()`.
    """
[docs]    def __init__(self, stimuli=None, name=None):
        if not isinstance(stimuli, (tuple, list)):
            self.stimuli = [stimuli]
        else:
            self.stimuli = stimuli
        if name is None:
            self.name = self.stimuli[0].name
        else:
            self.name = name
 
    def __getattr__(self, name):
        """Do whatever asked but per stimulus
        """
        def method(*args, **kwargs):
            outputs =[getattr(stim, name)(*args, **kwargs) for stim in self.stimuli]
            # see if only None returned, meaning that probably the function
            # doesn't return anything
            notnone = [o for o in outputs if o is not None]
            if len(notnone) != 0:
                return outputs
        try:
            return method
        except TypeError:
            return getattr(self, name)
    def __iter__(self):
        return self.stimuli.__iter__()
 
[docs]class MouseRespGroup(object):
[docs]    def __init__(self, win, stimuli, respmap=None, multisel=False,
                 on_color='#ff7260', off_color='white', pos=(0,0), name=''):
        #super(MouseRespGroup, self).__init__(stimuli=stimuli, name=name)
        self.win = win
        self.multisel = multisel
        self.on_color = on_color
        self.off_color = off_color
        self.pos = pos
        self.name = name
        if isinstance(stimuli, str):
            stimuli = [stimuli]
        self.stimuli = []
        for i, stim in enumerate(stimuli):
            if isinstance(stim, str):
                add = np.array([0, (len(stimuli)/2-i)*.2*1.5])
                stim = visual.TextStim(self.win, text=stim, height=.2,
                        pos=pos+add)
                stim.size = (1, .2)
                stim._calcSizeRendered()
                size = (stim._sizeRendered[0]*1.2, stim._sizeRendered[1]*1.2)
            else:
                stim._calcSizeRendered()
                size = stim._sizeRendered
            stim._calcPosRendered()
            stim.respbox = visual.Rect(
                                    self.win,
                                    name=stim.name,
                                    lineColor=None,
                                    fillColor=None,
                                    pos=stim._posRendered,
                                    height=size[1],
                                    width=size[0],
                                    units='pix'
                                    )
            stim.respbox.selected = False
            self.stimuli.append(stim)
        self.selected = [False for stim in self.stimuli]
        self.clicked_on = [False for stim in self.stimuli]
 
    def setPos(self, newPos):
        for stim in self.stimuli:
            stim.pos += self.pos - newPos
            stim.respbox.pos += self.pos - newPos
    def draw(self):
        for stim in self.stimuli:
            stim.draw()
            #stim.respbox.draw()
    def contains(self, *args, **kwargs):
        self.clicked_on = [stim.respbox.contains(*args, **kwargs) for stim in self.stimuli]
        #self.state = [(s and st) for s, st in zip(sel, self.state)]
        return any(self.clicked_on)
    def select(self, stim=None):
        if stim is None:
            try:
                idx = self.clicked_on.index(True)
            except:
                return
            else:
                stim = self.stimuli[idx]
            #if any(self.state):
                #for stim, state in zip(self.stimuli, self.state):
                    #if self.multisel:
                        #self._try_set_color(stim, state)
                    #else:
                        #self._try_set_color(stim, False)
                #if not self.multisel:
                    #self._try_set_color(stim, True)
        #else:
        if not self.multisel:
            for st in self.stimuli:
                if st == stim:
                    self._try_set_color(stim)
                else:
                    self._try_set_color(st, state=False)
        else:
            self._try_set_color(stim)
    def reset(self):
        for stim in self.stimuli:
            self._try_set_color(stim, state=False)
    def _try_set_color(self, stim, state=None):
        if state is None:
            if not stim.respbox.selected:
                color = self.on_color
                stim.respbox.selected = True
            else:
                color = self.off_color
                stim.respbox.selected = False
        else:
            if state:
                color = self.on_color
                stim.respbox.selected = True
            else:
                color = self.off_color
                stim.respbox.selected = False
        self.selected = [s.respbox.selected for s in self.stimuli]
        try:
            stim.setColor(color)
        except:
            stim.setLineColor(color)
            stim.setFillColor(color)
 
[docs]class OrderedDict(dict, DictMixin):
    """
    OrderedDict code (because some are stuck with Python 2.5)
    Produces an dictionary but with (key, value) pairs in the defined order.
    Created by Raymond Hettinger on Wed, 18 Mar 2009, under the MIT License
    <http://code.activestate.com/recipes/576693/>_
    """
[docs]    def __init__(self, *args, **kwds):
        if len(args) > 1:
            raise TypeError('expected at most 1 arguments, got %d' % len(args))
        try:
            self.__end
        except AttributeError:
            self.clear()
        self.update(*args, **kwds)
 
    def clear(self):
        self.__end = end = []
        end += [None, end, end]         # sentinel node for doubly linked list
        self.__map = {}                 # key --> [key, prev, next]
        dict.clear(self)
    def __setitem__(self, key, value):
        if key not in self:
            end = self.__end
            curr = end[1]
            curr[2] = end[1] = self.__map[key] = [key, curr, end]
        dict.__setitem__(self, key, value)
    def __delitem__(self, key):
        dict.__delitem__(self, key)
        key, prev, next = self.__map.pop(key)
        prev[2] = next
        next[1] = prev
    def __iter__(self):
        end = self.__end
        curr = end[2]
        while curr is not end:
            yield curr[0]
            curr = curr[2]
    def __reversed__(self):
        end = self.__end
        curr = end[1]
        while curr is not end:
            yield curr[0]
            curr = curr[1]
    def popitem(self, last=True):
        if not self:
            raise KeyError('dictionary is empty')
        if last:
            key = reversed(self).next()
        else:
            key = iter(self).next()
        value = self.pop(key)
        return key, value
    def __reduce__(self):
        items = [[k, self[k]] for k in self]
        tmp = self.__map, self.__end
        del self.__map, self.__end
        inst_dict = vars(self).copy()
        self.__map, self.__end = tmp
        if inst_dict:
            return (self.__class__, (items,), inst_dict)
        return self.__class__, (items,)
    def keys(self):
        return list(self)
    setdefault = DictMixin.setdefault
    update = DictMixin.update
    pop = DictMixin.pop
    values = DictMixin.values
    items = DictMixin.items
    iterkeys = DictMixin.iterkeys
    itervalues = DictMixin.itervalues
    iteritems = DictMixin.iteritems
    def __repr__(self):
        if not self:
            return '%s()' % (self.__class__.__name__,)
        return '%s(%r)' % (self.__class__.__name__, self.items())
    def copy(self):
        return self.__class__(self)
    @classmethod
    def fromkeys(cls, iterable, value=None):
        d = cls()
        for key in iterable:
            d[key] = value
        return d
    def __eq__(self, other):
        if isinstance(other, OrderedDict):
            return len(self)==len(other) and self.items() == other.items()
        return dict.__eq__(self, other)
    def __ne__(self, other):
        return not self == other
 
class _HTMLParser(HTMLParser):
    def handle_starttag(self, tag, attrs):
        if tag in self.tags:
            ft = '<font face="sans-serif">'
        else:
            ft = ''
        self.output += self.get_starttag_text() + ft
    def handle_endtag(self, tag):
        if tag in self.tags:
            ft = '</font>'
        else:
            ft = ''
        self.output += ft + '</' + tag + '>'
    def handle_data(self, data):
        self.output += data
    def feed(self, data):
        self.output = ''
        self.tags = ['h%i' %(i+1) for i in range(6)] + ['p']
        HTMLParser.feed(self, data)
        return self.output
[docs]def combinations(iterable, r):
    """
    Produces combinations of `iterable` elements of lenght `r`.
    Examples:
        - combinations('ABCD', 2) --> AB AC AD BC BD CD
        - combinations(range(4), 3) --> 012 013 023 123
    `From Python 2.6 docs <http://docs.python.org/library/itertools.html#itertools.combinations>`_
    under the Python Software Foundation License
    :Args:
        - iterable
            A list-like or a str-like object that contains some elements
        - r
            Number of elements in each ouput combination
    :Returns:
        A generator yielding combinations of lenght `r`
    """
    pool = tuple(iterable)
    n = len(pool)
    if r > n:
        return
    indices = range(r)
    yield tuple(pool[i] for i in indices)
    while True:
        for i in reversed(range(r)):
            if indices[i] != i + n - r:
                break
        else:
            return
        indices[i] += 1
        for j in range(i+1, r):
            indices[j] = indices[j-1] + 1
        yield tuple(pool[i] for i in indices)
 
[docs]def combinations_with_replacement(iterable, r):
    """
    Produces combinations of `iterable` elements of length `r` with
    replacement: identical elements can occur in together in some combinations.
    Example: combinations_with_replacement('ABC', 2) --> AA AB AC BB BC CC
    `From Python 2.6 docs <http://docs.python.org/library/itertools.html#itertools.combinations_with_replacement>`_
    under the Python Software Foundation License
    :Args:
        - iterable
            A list-like or a str-like object that contains some elements
        - r
            Number of elements in each ouput combination
    :Returns:
        A generator yielding combinations (with replacement) of length `r`
    """
    pool = tuple(iterable)
    n = len(pool)
    if not n and r:
        return
    indices = [0] * r
    yield tuple(pool[i] for i in indices)
    while True:
        for i in reversed(range(r)):
            if indices[i] != n - 1:
                break
        else:
            return
        indices[i:] = [indices[i] + 1] * (r - i)
        yield tuple(pool[i] for i in indices)
 
[docs]def try_makedirs(path):
        """Attempts to create a new directory.
        This function improves :func:`os.makedirs` behavior by printing an
        error to the log file if it fails and entering the debug mode
        (:mod:`pdb`) so that data would not be lost.
        :Args:
            path (str)
                A path to create.
        """
        if not os.path.isdir(path) and path not in ['','.','./']:
            try: # if this fails (e.g. permissions) we will get an error
                os.makedirs(path)
            except:
                logging.error('ERROR: Cannot create a folder for storing data %s' %path)
                # FIX: We'll enter the debugger so that we don't lose any data
                import pdb; pdb.set_trace()
 
[docs]def signal_det(corr_resp, subj_resp):
    """
    Returns an accuracy label according the (modified) Signal Detection Theory.
    ================  ===================  =================
                      Response present     Response absent
    ================  ===================  =================
    Stimulus present  correct / incorrect  miss
    Stimulus absent   false alarm          (empty string)
    ================  ===================  =================
    :Args:
        corr_resp
            What one should have responded. If no response expected
            (e.g., no stimulus present), then it should be an empty string
            ('')
        subj_resp
            What the observer responsed. If no response, it should be
            an empty string ('').
    :Returns:
        A string indicating the type of response.
    """
    if corr_resp == '':  # stimulus absent
        if subj_resp == '':  # response absent
            resp = ''
        else:  # response present
            resp = 'false alarm'
    else:  # stimulus present
        if subj_resp == '':  # response absent
            resp = 'miss'
        elif corr_resp == subj_resp:  # correct response present
            resp = 'correct'
        else:  # incorrect response present
            resp = 'incorrect'
    return resp
 
[docs]def invert_dict(d):
    """
    Inverts a dictionary: keys become values.
    This is an instance of an OrderedDict, and so the new keys are
    sorted.
    :Args:
        d: dict
    """
    inv_dict = dict([[v,k] for k,v in d.items()])
    sortkeys = sorted(inv_dict.keys())
    inv_dict = OrderedDict([(k,inv_dict[k]) for k in sortkeys])
    return inv_dict
 
def get_version():
    """Get psychopy_ext version
    If using a repository, then git head information is used.
    Else version number is used.
    :Returns:
        A key where to store version in `self.runtime_info` and
        a string value of psychopy_ext version.
    """
    d = os.path.abspath(os.path.dirname(__file__))
    githash = psychopy.info._getHashGitHead(dir=d) # should be .../psychopy/psychopy/
    if not githash:  # a workaround when Windows cmd has no git
        git_head_file = os.path.join(d, '../.git/HEAD')
        try:
            with open(git_head_file) as f:
                pointer = f.readline()
            pointer = pointer.strip('\r\n').split('ref: ')[-1]
            git_branch = pointer.split('/')[-1]
            pointer = os.path.join(d, '../.git', pointer)
            with open(pointer) as f:
                git_hash = f.readline()
            githash = git_branch + ' ' + git_hash.strip('\r\n')
        except:
            pass
    if githash:
        key = 'pythonPsychopy_extGitHead'
        value = githash
    else:
        key = 'pythonPsychopy_extVersion'
        value = psychopy_ext_version
    return key, value
[docs]def get_mon_sizes(screen=None):
    """Get a list of resolutions for each monitor.
    Recipe from <http://stackoverflow.com/a/10295188>_
    :Args:
        screen (int, default: None)
            Which screen's resolution to return. If None, the a list of all
            screens resolutions is returned.
    :Returns:
        a tuple or a list of tuples of each monitor's resolutions
    """
    app = wx.App(False)  # create an app if there isn't one and don't show it
    nmons = wx.Display.GetCount()  # how many monitors we have
    mon_sizes = [wx.Display(i).GetGeometry().GetSize() for i in range(nmons)]
    if screen is None:
        return mon_sizes
    else:
        return mon_sizes[screen]
 
[docs]def get_para_no(file_pattern, n=6):
    """Looks up used para numbers and returns a new one for this run
    """
    all_data = glob.glob(file_pattern)
    if all_data == []: paranos = random.choice(range(n))
    else:
        paranos = []
        for this_data in all_data:
            lines = csv.reader( open(this_data) )
            try:
                header = lines.next()
                ind = header.index('paraNo')
                this_parano = lines.next()[ind]
                paranos.append(int(this_parano))
            except: pass
        if paranos != []:
            count_used = np.bincount(paranos)
            count_used = np.hstack((count_used,np.zeros(n-len(count_used))))
            poss_paranos = np.arange(n)
            paranos = random.choice(poss_paranos[count_used == np.min(count_used)].tolist())
        else: paranos = random.choice(range(n))
    return paranos
 
[docs]def get_unique_trials(trial_list, column='cond'):
    unique = []
    conds = []
    for trial in trial_list:
        if trial[column] not in conds:
            unique.append(OrderedDict(trial))
            conds.append(trial[column])
    # this does an argsort
    order = sorted(range(len(conds)), key=conds.__getitem__)
    # return an ordered list
    return [unique[c] for c in order]
 
def weighted_sample(probs):
        warnings.warn("weighted_sample is deprecated; "
                      "use weighted_choice instead")
        return weighted_choice(weights=probs)
[docs]def weighted_choice(choices=None, weights=None):
    """
    Chooses an element from a list based on it's weight.
    :Kwargs:
        - choices (list, default: None)
            If None, an index between 0 and ``len(weights)`` is returned.
        - weights (list, default: None)
            If None, all choices get equal weights.
    :Returns:
        An element from ``choices``
    """
    if choices is None:
        if weights is None:
            raise Exception('Please specify either choices or weights.')
        else:
            choices = range(len(weights))
    elif weights is None:
        weights = np.ones(len(choices)) / float(len(choices))
    if not np.allclose(np.sum(weights), 1):
        raise Exception('Weights must add up to one.')
    which = np.random.random()
    ind = 0
    while which>0:
        which -= weights[ind]
        ind +=1
    ind -= 1
    return choices[ind]
 
[docs]def get_behav_df(subjid, pattern='%s'):
    """
    Extracts data from files for data analysis.
    :Kwargs:
        pattern (str, default: '%s')
            A string with formatter information. Usually it contains a path
            to where data is and a formatter such as '%s' to indicate where
            participant ID should be incorporated.
    :Returns:
        A `pandas.DataFrame` of data for the requested participants.
    """
    if type(subjid) not in (list, tuple):
        subjid_list = [subjid]
    else:
        subjid_list = subjid
    df_fnames = []
    for subjid in subjid_list:
        fnames = glob.glob(pattern % subjid)
        fnames.sort()
        df_fnames += fnames
    dfs = []
    for dtf in df_fnames:
        data = pandas.read_csv(dtf)
        if data is not None:
            dfs.append(data)
    if dfs == []:
        print df_fnames
        raise IOError('Behavioral data files not found.\n'
            'Tried to look for %s' % (pattern % subjid))
    df = pandas.concat(dfs, ignore_index=True)
    return df
 
[docs]def latin_square(n=6):
    """
    Generates a Latin square of size n. n must be even.
    Based on `Chris Chatham's suggestion
    <http://rintintin.colorado.edu/~chathach/balancedlatinsquares.html>`_
    :Kwargs:
        n (int, default: 6)
            Size of Latin square. Should be equal to the number of
            conditions you have.
    .. :note: n must be even. For an odd n, I am not aware of a
              general method to produce a Latin square.
    :Returns:
        A `numpy.array` with each row representing one possible ordering
        of stimuli.
    """
    if n%2 != 0:
        raise Exception('n must be even!')
    latin = []
    col = np.arange(1,n+1)
    first_line = []
    for i in range(n):
        if i%2 == 0:
            first_line.append((n-i/2)%n + 1)
        else:
            first_line.append((i+1)/2+1)
    latin = np.array([np.roll(col,i-1) for i in first_line])
    return latin.T
 
[docs]def make_para(n=6):
    """
    Generates a symmetric para file with fixation periods approximately 25%
    of the time.
    :Kwargs:
        n (int, default: 6)
            Size of Latin square. Should be equal to the number of
            conditions you have.
            :note: n must be even. For an odd n, I am not aware of a
            general method to produce a Latin square.
    :Returns:
        A `numpy.array` with each row representing one possible ordering
        of stimuli (fixations are coded as 0).
    """
    latin = latin_square(n=n).tolist()
    out = []
    for j, this_latin in enumerate(latin):
        this_latin = this_latin + this_latin[::-1]
        temp = []
        for i, item in enumerate(this_latin):
            if i%4 == 0:
                temp.append(0)
            temp.append(item)
        temp.append(0)
        out.append(temp)
    return np.array(out)