# 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)