Q) Is there any example for fMRI experiment?

3,775 views
Skip to first unread message

Jason Love

unread,
May 4, 2012, 10:08:09 AM5/4/12
to psychop...@googlegroups.com
Hello psychopy users 
I've just migrated to psychopy from presentation and am about to write a script for my new fMRI experiment. 
Because I'm new to python, I'm thinking of constructing the overall flow using builder view and then add some more components that are necessary for fMRI experiment in the code view.
It'd be very helpful if I can see some example codes and I'm wondering if there is any way to get those for fMRI experiments. 
Any advice is much appreciated

Jason 


Jeremy Gray

unread,
May 4, 2012, 10:52:57 AM5/4/12
to psychop...@googlegroups.com
Hi Jason,

I don't have a handy script I can just send, I'm hoping someone else can do so. (I have scanned using psychopy, but it was a very unusual design, with 45-min long scans.)

Using the builder and then tweaking as necessary is often a great way to go. For simple fMRI paradigms, it should be relatively easy, and its likely that many of the needed tweaks can be accomplished through code components.

As you probably know, the two biggest considerations for fMRI (above and beyond a regular behavioral study) are a) how to sync to the scanner, and b) how to analyze the data afterwards (esp to extract timing info for trial onsets).

For sync'ing, check out the fMRI_launchScan.py demo in coder demos (under "experiment control"). This is intended to help test and debug sync issues without needing any hardware (such as a scanner, or a sync-pulse generator). Instead in software, it generates a key-board based sync pulse (default = "5").

For data analysis, you'll probably want to use the saveAsWideText option when saving the data output. Its good to record timing in terms of time since the start of the scanning run. This is much better than trying to reconstruct onset times using cumulative times of multiple events.

--Jeremy



Jason 


--
You received this message because you are subscribed to the Google Groups "psychopy-users" group.
To post to this group, send email to psychop...@googlegroups.com.
To unsubscribe from this group, send email to psychopy-user...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/psychopy-users?hl=en.

Erik Kastman

unread,
May 4, 2012, 12:59:06 PM5/4/12
to psychop...@googlegroups.com
Hi Jason,

I've been developing a handful of fMRI experiments over the past few months and would be happy to share my experience (and experiments). Like Jeremy said, the biggest concerns with fMRI involve syncing the timing between the stimulus presentation and the scanner's acquisitions. Scanners are set up to send a sync pulse for each TR, which is recorded at different sites and scanners as either a keyboard stroke coming in over an emulated USB keyboard, or a voltage change on a parallel port pin. When I was using presentation we had a parallel port, but I believe Presentation can monitor pulses from a keyboard as well. Figuring out how your scanner sends its sync pulses will determine how you setup the experiment to listen to pulses. The MR Techs or Physicists running your scanner should be able to tell you how the pulses are sent.

Since PsychoPy has its roots in visual presentation, its emphasis was on displaying stimuli for very precise durations, but not necessarily at very precise times. Therefore, with the small amount of overhead that occurs checking routine/flow conditions and entering/exiting flow loops, I've found that I'll get somewhere around 8-10 seconds worth of "wall clock" time drift in a 5 minute experiment with what should be a "10 second" trial length. For example, if I have an experiment with 30 10s trials, it should in theory last 300 seconds, but actually lasts 310 or so. The drift hasn't seemed to vary too much between runs, so if you collect 312 seconds of data (156 2s TRs) from the scanner, and set your experiment to close at exactly 312 seconds, the experiment and scanner should finish within milliseconds of each other. That way you'll know that the onset times you collected were accurate and can be matched up to your BOLD images in the model.

I'm actually in the middle of checking this with some of our other users, but I believe that for standard (block and fast event-related) experiments this is fine, since a small amount of drift will shift the real acquisition time across a TR, essentially jittering the experiment and increasing the acquisition time resolution. The critical thing is that the onset times are correct, or the model will be off. This means the second part of Jeremy's comment, getting accurate onset times, is critical. 

If you're concerned about maintaining the trial onset lock-step with the scanner acquisition, you should include some space at the end of the trial loop (typically a trial feedback routine or fixation) and write a check to count received reps, and start the next trial at precisely the timing of the next pulse. This is a similar, but not identical strategy to what Presentation does, where you specify the exact pulse that a picture should be presented on:

picture $p1; mri_pulse=4; time=0; target_button = 2; code=101;
picture $p2; mri_pulse=6; time = 500;   target_button = 1; code=102; #4.5
picture $null; mri_pulse=7; time = 1500; code=100; #7.5

I haven't written any pulse-waiting code for PsychoPy yet, but may do so in the near future. Currently PsychoPy's includes a way to sync the start of the experiment using the fMRI launchScan that you can see in the fMRI coder demo, but does not include support for counting sync pulses, although it shouldn't be too hard to find a way to do that. I typically do this by starting an experiment with an instruction routine, then following that with a waitForScanner routine. In the waitForScanner, there's one code component called waitForMriCode with the following:

waitForMriCode Begin Experiment:
from psychopy.hardware.emulator import launchScan
#
# settings for launchScan:
MR_settings = { 
    'TR': 2.000, # duration (sec) per volume
    'volumes': 210, # number of whole-brain 3D volumes / frames
    'sync': 'equal', # character to use as the sync timing event; assumed to come at start of a volume
    'skip': 4, # number of volumes lacking a sync pulse at start of scan (for T1 stabilization)
    }

waitForMriCode Begin Routine:
vol = launchScan(win, MR_settings, globalClock=logging.defaultClock, mode=expInfo['mriMode']) 

I then have a setting in the builder's Experiment Settings called 'mriMode' which is set to either 'scan' or 'test', following the launchScan demo. That will hold the experiment until the scanner sends a pulse that indicates it is ready to begin (in this case, after 4 skipped discarded acquisitions (discdacqs) ). 


Someone please correct me if I'm wrong, but I think all that you'll primarily need for most experiments is to 1) sync the starting time and 2) to record the onsets carefully so that you can later enter them into the model. I've been parsing my logfiles to get outsets in a pretty hackneyed way, but will also be writing an easy onset extractor in the next month or two.

Jason, we may be able to help you more if you send more detail about your task, or have some more specific questions after looking at the fMRI_launchScan demo. Feel free to send to me or the list, and we'll try to help if we can.


(I have scanned using psychopy, but it was a very unusual design, with 45-min long scans.)

What kind of task was _that_, Jeremy? 2-Minute TR? ;)

Erik

Jeremy Gray

unread,
May 4, 2012, 2:00:27 PM5/4/12
to psychop...@googlegroups.com
waitForMriCode Begin Routine:
vol = launchScan(win, MR_settings, globalClock=logging.defaultClock, mode=expInfo['mriMode']) 

I think all that you'll primarily need for most experiments is to 1) sync the starting time and 2) to record the onsets carefully so that you can later enter them into the model.

this looks right to me. just to elaborate Erik's points and usage: launchScan accomplishes objective #1 = wait for the first scan pulse. Note that if your pulse sequence does not send a few initial pulses ("disdaqs" for T1 stabilization) then launchScan will NOT magically know about them. so you have to keep track of what you mean by "the start of the scan".

And then Erik's "globalClock=logging.defaultClock" looks like its doing objective #2 in a somewhat sneaky way. I think its resetting the clock that's used for psychopy's logging back to zero when the scan starts. so then the log timing values can be used as the stimulus onsets. logging can be used in the way, but is not generally intended to be so. (do whatever you like, of course!) You could give it any clock object, and that would get reset to 0.000 upon receiving the first sync pulse. in the demo, this clock is called globalClock.

Instead of resetting the logging clock, you could write "Start of scan" into the logging, and then subtract that time from your other event times to arrive at time relative to the start of the scan.

logging.data('THIS-IS-THE-START-OF-THE-SCAN')

you want this label to be very clear and distinctive, so that there's no ambiguity when doing the post-processing.
(I have scanned using psychopy, but it was a very unusual design, with 45-min long scans.)
What kind of task was _that_, Jeremy? 2-Minute TR? ;)

a meditation study, 2s TRs. :-)

--Jeremy


 

Jason Love

unread,
May 4, 2012, 10:56:12 PM5/4/12
to psychop...@googlegroups.com
Thanks guys for your great advice and detailed explanations. 

- Jason

Jeremy Gray

unread,
May 4, 2012, 11:36:40 PM5/4/12
to psychop...@googlegroups.com
I've hacked together a skeleton version of a builder fMRI study, attached. Along the way, I see several things that could be useful, but they are not there yet. I've also just adopted Erik's ingenious usage of the logging clock as a handy way to generate time-stamped output (even though I do think some other way to do it would be desirable). So this is truly minimal and I hope to improve it as time allows.

Limitations:
- for now, stick with using 5 as the sync key. 'a' or '=' did not work for me. will fix at some point.
- note that some of the scan parameters are in the expInfo gui, but some (like sound for test mode) are done through the code component in "wait"
- if you run using the defaults, to have to press key 5 to keep it moving along. if you set mode to 'test', it will run as if a scanner is controlling the pacing of the script.
- look at the data/*log file, especially the time stamps

--Jeremy
fMRI.psyexp

Jonathan Peirce

unread,
May 5, 2012, 12:26:12 PM5/5/12
to psychop...@googlegroups.com

On 04/05/2012 17:59, Erik Kastman wrote:
I'll get somewhere around 8-10 seconds worth of "wall clock" time drift in a 5 minute experiment with what should be a "10 second" trial length. For example, if I have an experiment with 30 10s trials, it should in theory last 300 seconds, but actually lasts 310 or so.
PsychoPy is absolutely capable of running experiments with zero drift, but the most common way of writing scripts is by a clock that is reset for each epoch/stimulus. e.g.::

trialClock = core.Clock()
for thisEpoch in epochs:
    trialClock.reset()
    while trialClock.getTime()<epochDuration:
        #present stimuli etc
That's super-easy to code, for precise timing of an individual trial it's fine, and when you end a trial based on a subject's response it is the only way to work. BUT it always results in one extra screen frame (or fraction of one) per epoch/trial. If you present 200 stimuli using this method you'll overshoot by roughly 200/60 seconds on a standard projector.

To get around it you should either code to use a global clock or insert something like a routine that waits for a scanner pulse for, say, 1/2 second before the required epoch. The global clock method would look something like this::

globalClock = core.Clock()
endTime=0
for thisEpoch in epochs:
    endTime = endTime+epochDuration
    while globalClock.getTime()<endTime:
        #present stimuli etc
With this method, each epoch will accommodate for the half frame extra/less in the previous.

Unfortunately Builder only uses the relative timing method so far. If the trial ends when subjects make a response that is the really the only way to go. But we could probably make it smarter so that, it switches to using a global clock when that's possible.

Jon
-- 
Jonathan Peirce
Nottingham Visual Neuroscience

http://www.peirce.org.uk/

Erik Kastman

unread,
May 6, 2012, 11:39:45 AM5/6/12
to psychop...@googlegroups.com
globalClock=logging.defaultClock

Good catch in calling this out! In general one shouldn't be messing with PyschoPy's internals like this (it can have unexpected effects that make troubleshooting difficult), but for the very limited case when all that's being shown is an instruction screen without any data or trials to log, and there's good reason to reset the clock (simplicity of onsets), it may be a satisfactory solution. I suppose it is also sneaky, though, since I didn't even notice that I should have explained it. 

- for now, stick with using 5 as the sync key. 'a' or '=' did not work for me. will fix at some point

For an "=", you'll want to spell out 'equal'. No idea why 'a' wasn't working - which version were you testing on? 


Jon said:
PsychoPy is absolutely capable of running experiments with zero drift, but the most common way of writing scripts is by a clock that is reset for each epoch/stimulus. e.g.::

Thanks for clarifying the two different timing strategies - I didn't mean to say that PsychoPy couldn't keep accurate time, just that that wasn't the way that Builder currently does it now.

This is a great example of why the flexibility of the library is so essential - whichever way you want to do it, it can be done. For me, the drift that arises from Builder's current strategy was an acceptable tradeoff (given jittering concerns, etc) for the semantic descriptiveness that builder provides, but if other people want to follow a global clock that's a small tweak, too. 

Unfortunately Builder only uses the relative timing method so far

I'm interested in expanding this - let's talk more on the dev list.

Jeremy Gray

unread,
Dec 16, 2013, 8:46:49 AM12/16/13
to Fernanda Palhano, psychop...@googlegroups.com
Hi Fernanda,

I'm replying to the whole list.

The error you get in that old script is due to changes in PsychoPy
since that time. The error can be avoided by doing
globalClock=core.Clock()
in the call to launchScan (in wait4scan, Begin routine code):
launchScan(win, MRinfo,
mode=expInfo['mode'],
globalClock=core.Clock())

I've never had to use a parallel port, so I am unsure about how to
wait for a sync pulse that way. I expect it would be useful for
several people, if you do come up with a solution.

--Jeremy
--Jeremy


On Mon, Dec 16, 2013 at 8:23 AM, Fernanda Palhano
<nandap...@gmail.com> wrote:
> Hi Jeremy,
>
> I'm not sure if you remember this post, but I'm having trouble synchronizing
> my fMRI experiment using a parallel port trigger. Looking for some help,
> I've found your code (fMRI.pysexp) here.
> When I try to run it, I get this error message:
>
> File "/Users/fernandap/Data/teste_psychopy/fMRI_lastrun.py", line 85, in
> <module>
> globalClock=logging.defaultClock)
> File
> "/Applications/PsychoPy2.app/Contents/Resources/lib/python2.7/psychopy/hardware/emulator.py",
> line 265, in launchScan
> globalClock.reset()
> AttributeError: MonotonicClock instance has no attribute 'reset'
>
> Indeed, I was trying to adapt your idea to my experiment
> (regEmo_sessao1.psyexp), changing the sync part to wait for a pulse in the
> parallel port.
>
> Any help would be much appreciated,
>
> Fernanda

Erik Kastman

unread,
Dec 16, 2013, 7:47:49 PM12/16/13
to psychop...@googlegroups.com, Fernanda Palhano
Hi Fernanda,

Jeremy is exactly right - reseting globalClock is “cheating” and newer versions of Psychopy will raise the error that you see. If you follow his advice and simply make a new clock (see fmriClock below) you can reset that without trouble. 

Unfortunately (fortunately for me!) I never had to write the kind of wait-for-each-sync code that I mentioned in that earlier thread. I do have some code that waits for the first pulse over a parallel port which has worked well for me, and is probably sufficient for what you need, especially since the routines in the experiment you sent use non-slip timing ( http://www.psychopy.org/general/timing/nonSlipTiming.html ).

I expect that you’ve probably figured this out, but a known-good example never hurts. Feel free to ask any other questions if they come up!

Erik

# If using Builder, Include these in “Begin Experiment"
from psychopy.hardware.emulator import launchScan
fmriClock = core.Clock()
address = 0x378
wait_msg = "Waiting for scanner…”
waitMsgStim = visual.TextStim(win, color='DarkGray', text=wait_msg)

# If using Builder, include these in “Begin Routine” of a waitForScanner Routine
pinStatus = winioport.inp(address)
waitMsgStim.draw()
win.flip()
while True:
    if pinStatus != winioport.inp(address):
        break  # start exp when any pin values change
    fmriClock.reset()
    logging.exp('parallel trigger: start of scan')
    win.flip()  # blank the screen on first sync pulse received


To unsubscribe from this group and stop receiving emails from it, send an email to psychopy-user...@googlegroups.com.

To post to this group, send email to psychop...@googlegroups.com.

Erik Kastman

unread,
Dec 17, 2013, 1:01:54 PM12/17/13
to psychop...@googlegroups.com, Fernanda Palhano
Hi Fernanda,

If psychopy.parallel is working then by all means use it! It’s been almost a year and a half since I looked at the winioport code so I may have forgotten something. 

A few thoughts: 

* There are newer class-based APIs for dealing with the parallel port (PParallelLinux, PParallelInpOut32 and PParallelDLPortIO), so you can try those if you have problems. However, I’d recommend sticking with what’s working for you already unless you have problems or need some of the newer functions (i.e. writing to different parallel ports from the same experiment).

* If you know for a fact that your trigger is coming in on a specific pin (pin10 in your case) you should definitely listen only for that pin since you will get better latency - checking against any pins (just seeing if any of the pins have changed status) is a way to be agnostic about the trigger, but it comes with the cost of slightly less temporal certainty.

* It’s also good to note for other users reading the list that you’ve found the correct port address for your setup (0x1110) and not just used the default address (0x0378 in my sample code).

* The globalClock that psychopy uses internally will never reset (in fact trying to reset it causes the error with the monotonic clock that you first reported), so it’s alright that it doesn’t zero out the .log file. If you want to record timings for regressors, you should create a clock to use for reference to the scanner: fmriClock=core.Clock() . Then fmriClock.reset() after the first trigger is received and fmriClock.getTime() to log trial events. That will give you the elapsed time since the clock was last reset (i.e. the trigger was received). Then you can add that to your DataHandler with trials.addData() http://www.psychopy.org/api/data.html#psychopy.data.TrialHandler.addData It’s a little more work upfront since psychopy doesn’t record everything for you, but better to be explicitly logging events each run anyway.

* Since Sol’s IOHub is in good shape, I wonder if it might be worth revisiting to see if that can be used for listening for triggers as well? However, if you don’t feel like digging in to it, I think your solution should work just fine as well - just make sure that you start listening for the trigger slightly before the time you expect it (i.e. have a trial length that’s just short of the multiple of your TR) so that you can go into the next trial as soon as the signal is received and you don’t slip slightly over it. 

Let us know how it turns out!

Erik


On Dec 17, 2013, at 12:11 PM, Fernanda Palhano <nandap...@gmail.com> wrote:

Hi Erik,

Thank you  for your reply. 

To wait for the first pulse I was using the following code:
in "Begin Experiment"
from psychopy import parallel
parallel.setPortAddress(0x1110)
globalClock = core.Clock()
wait_msg="waiting for scanner..."
msg = visual.TextStim(win, color='DarkGray', text=wait_msg)

in "Begin Routine"
msg.draw()
win.flip()
while True:
  if parallel.readPin(10) == 1:
    break
globalClock.reset()

which seems to work fine. When I tryed to run yours, I have a problem: the message "waiting for scanner..." appears only for a frame. 
What I'm doing now is trying to synchronize each trial (adapting from Jeremy's code). Since my trial's duration is multiple of 2 and my TR=2, I think it is ok. What do you think about it?
One more question about the Clock. I'm not sure if I'm reseting and using a global clock. When I look my log file, the time doesn't seem to reset with the trigger.
I attached my code and a .log file (as I'm at my office now, I'm sending a pulse manually, and maybe the TR isn't exactly 2s)

Thank you very much again,
Fernanda


<RegEmo_sessao_teste_curto.psyexp><_2013_dez_17_1447.csv><_2013_dez_17_1447.log><_2013_dez_17_1447.psydat>

Reply all
Reply to author
Forward
0 new messages