short-duration visual stimulus X refresh rate confound

The problem is as follows. A typical monitor refreshes 60-120 times a second, the time between refreshes being 1000/60 = 16.667 ms. If you want to show your participant a stimulus for only a very brief period, with traditional lab-based software, algorithms sync up when a screen refresh occurs with the presentation of the stimulus. Indeed, in DMDX, the shortest duration stimuli are even specified in ‘ticks’, or the time between intervals.

On the web, however, there is currently no way for the browser or for Flash to know exactly when a refresh is going to happen, and thus no way of synching the display of stimuli to the refresh. Some packages get around this by getting you to download an exe file to run on your computer. Windows typically freaks out with exe files, showing you this sort of message:

Web based code just asks the browser to show/remove the stimulus from the display, with no regard for screen refreshes. There are several consequences:

  • stimuli shown for durations shorter than the refresh rate of the screen
    • don’t get shown.
    • are shown for too long.
    • appear/disappear on the screen at the wrong time.
  • longer duration stimuli:
    • are shown for too long/short a time.
    • appear/disappear at the wrong time.
    • start and stop at the correct time!

So I thought to simulate this. Here’s my thinking:

  • As we don’t know when the screen refreshes, I will add x to the start/end time of a stimulus, x being randomly chosen and ranging 0 up to the current refresh rate (flat distribution).
  • I’ll use an interval of 16ms for the refresh rate.
  • Vary the duration of the stimulus, from 1ms upwards
  • 10,000 x I’ll calculate the actual start time and end time of a stimulus and actual duration.
  • calculate the frequencies for the different start-time+durations.
  • plot the below graph in ye-olde excel.

 

Considerations

  • if you have a short duration stimuli, shown at a near perceptual threshold, if that stimulus is not shown 50% of the time, you’ve just massively diluted any chance of your finding (via stats) evidence for your participants detecting the stimulus.
  • Some stimuli may actually (rarely) be presented for the right amount of time! But, they start and end at the wrong time. Ouch.
package
{
 import flash.display.Sprite;
 import flash.net.FileReference;
 import flash.text.TextField;

 public class Simulate extends Sprite
 {
 private var txt:TextField;
 
 private var simulations:int = 10000;
 private var results:Array = [];
 //http://en.wikipedia.org/wiki/Refresh_rate. typically 60 on LCDs
 private var refresh:Number = 16.6666;
 private var result:Array;
 private var combined:Array = [];
 
 public function Simulate()
 {
 txt = new TextField();
 txt.text="hello";
 this.addChild(txt);
 
 
 
 //stimulusDuration();
 crunchStartStopDurs();
 
 }
 
 private function crunchStartStopDurs():void
 {
 
 function stringify(obj:Object):String{
 var arr:Array = [];
 for(var key:String in obj){
 arr.push(key+","+obj[key]);
 }
 arr.sort(Array.DESCENDING);
 return arr.join(",");
 }
 
 var chunked:Object;
 for(var i:int=0;i<30;i++){
 result = simulate(refresh,i+1,'');
 chunked = Chunkify.chunk(result)
 results.push ( stringify(chunked) );
 }

 save(results.join("n"));
 } 
 
 
 
 
 //varying duration, actual averages pretty good. 
 private function stimulusDuration():void
 {

 refresh = 13;
 
 for(var i:int=0;i<10;i++){
 result = simulate(refresh,i+5,'actualDuration');
 results.push ( result );
 }
 
 for(i = 0; i<results.length;i++){
 combined[combined.length] = i+5 +"," + Stats.average(results[i])+"," + Stats.median(results[i]);
 }
 
 trace(JSON.stringify(combined))
 //save('stimDuration',combined.join("n"));
 }
 

 
 private function simulate(refresh:int, stimDuration:int, dv:String):Array
 {
 var info:Array = [];
 var result:Object;
 
 for(var i:int=0;i<simulations;i++){
 Trial.stimulusDuration = stimDuration;
 result = Trial.result();
 if(dv!='') info[info.length] = result[dv];
 else info[info.length] = result;
 } 
 return info;
 } 
 
 
 private function save(resultsStr:String):void
 {
 var file:FileReference = new FileReference;
 var filepath:String = new String("results.sav");
 file.save( resultsStr, filepath );
 }
 
 }
}


package
{
 public class Trial
 {
 //http://en.wikipedia.org/wiki/Refresh_rate. typically 60 on LCDs
 public static var refresh:Number = 16.6666;
 public static var stimulusDuration:int=15;
 public static var start:int;

 
 private static var dif:int;
 
 public static function result():Object
 { 
 //test();
 return calc(refresh, Math.random()*refresh, stimulusDuration);
 }
 
 
 
 private static function test():void
 {
 
 trace(10 == calc(10, 0, 10).duration);
 trace(18 == calc(9, 0, 10).duration);
 trace(11 == calc(11, 0, 10).duration);
 trace(10 == calc(10, 1, 10).duration);
 trace(100 == calc(10, 1, 100).duration);
 trace(108 == calc(9, 0, 100).duration);
 trace(100 == calc(100, 0, 90).duration);
 trace(100 == calc(100, 50, 90).duration);
 }
 
 
 
 private static function calc(_refresh:int, _start:int, _duration:int):Object
 { 
 var _result:Object ={};
 
 _result.start = _start;
 _result.finish= _duration + _start;
 _result.duration= _duration;
 _result.refresh= _refresh;
 
 _result.actualStart = calcDifference(_start,_refresh);
 
 _result.actualFinish = calcDifference(_duration + _start, _refresh);
 
 _result.actualDuration = _result.actualFinish - _result.actualStart;

 
 //trace(JSON.stringify(_result))
 return _result;
 }
 
 
 private static function calcDifference(time:int, refresh:int):int{
 
 dif = time % refresh;
 
 if(dif>0){
 return time - dif + refresh;
 }
 
 return time ;
 

 }
 
 }
}

package
{
 public class Chunkify
 {
 
 
 
 
 public static function chunk(arr:Array):Object
 {
 var chunks:Object = {};
 var len:int = arr.length;
 //trace(JSON.stringify(arr));
 for(var i:int=0;i<len;i++){
 calc(chunks, arr[i]);
 }
 
 for(var key:String in chunks){
 chunks[key] = Stats.round(chunks[key] / len * 100,2);
 }
 
 chunks.duration = arr[0].duration;
 chunks.refresh = arr[0].refresh;
 
 
 trace(JSON.stringify(chunks))
 return chunks;
 }
 
 private static function calc(chunks:Object, info:Object):void
 {
 var id:String = info.actualStart +"_"+info.actualDuration;
 if(chunks.hasOwnProperty(id)==false) chunks[id] = 1;
 else chunks[id]++;
 }
 }
}

package
{
 public class Stats
 {
 static public function median(plug:Array):Number
 {
 // Even length.
 if(plug.length % 2 == 0)
 {
 var a:Number = plug[int(plug.length / 2) - 1];
 var b:Number = plug[int(plug.length / 2)];
 
 return (a + b) / 2;
 }
 
 // Odd length.
 return plug[int(plug.length / 2)];
 }
 
 public static function average(aArray:Array):Number {
 return sum(aArray) / aArray.length;
 }
 
 public static function sum(aArray:Array):Number {
 var nSum:Number = 0;
 for(var i:Number = 0; i < aArray.length; i++) {
 if(typeof aArray[i] == "number") {
 nSum += aArray[i];
 }
 }
 return nSum;
 }
 
 public static function round(numberVal:Number, precision:int = 0):Number{
 var decimalPlaces:Number = Math.pow(10, precision);
 return Math.round(decimalPlaces * numberVal) / decimalPlaces;
 }
 
 }
}

 

One Reply to “short-duration visual stimulus X refresh rate confound”

Leave a Reply

Your email address will not be published. Required fields are marked *