Skip to content

Laboratory of Neuropsychology

NIMH Cortex Programming Techniques

Cortex problems

Cortex looses spikes

NIMH Cortex will loose a small percent of spikes during recording. As long as the peak firing rate is below about 900 spikes/channel on any channel, the loss is under 1% and appears quite random. The Windows XP version of Cortex (VCortex 2.x) looses more spikes than the DOS version. There is new spike latch circuit DOS Cortex and a modified version of DOS Cortex that reduces the number of missed spikes to nearly zero. 

The time between trials is variable

You cannot know how much time has elapsed between the end of one trial and the start of the next. The only solution is to build an external clock circuit and have Cortex read the clock to keep track of "lost time."

The shortest interval that Cortex can accurately time is about 2 ms, unless you use a hardware timer.

Setting the cortex timer to 1 ms has no meaning. That is, the commands

MS_TIMERset(1,1);
while(MS_TIMERcheck(1));


can give .99 ms delay or no delay at all. There is always an uncertainty of 1 ms. There are two solutions.

1. Sychronizing with the interrupt service routine. This example generates a pulse close to 1 ms in duration.

MS_TIMERset(1,1);
while(MS_TIMERcheck(1)); // wait for the timer to synchronize with the interrupt
DEVoutp(1,1,1); // turn on the bit of an I/O port
MS_TIMERset(1,1);
while(MS_TIMERcheck(1)); // this cycle will finish in about 1ms
DEVoutp(1,1,0); // turn bit off

2. Use the Precision Timer hardware developed for Cortex

PRECISION_TIMERset(1.0, PTIMER_CHAN0) ; // Set Precision Timer for 1 ms pulse, output to channel 0
PRECISION_TIMERstart(); // Start pulse
while(!PRECISION_TIMERdone); // Wait for timer to end.

Cortex timing is unreliable at the start of a trial

When Cortex starts a trial there is an ambiguity in the timing that can show up when trying to synchronize with external data acquisition systems (e.g., Plexon). Wait a few milliseconds, then send an event code to the external data acquisition system.

Cortex random number generators can cause problems

Cortex has two random number generators: rand() and rand2(). The documentation for these is hard to understand. rand() is used for the trial blocking and sequencing. It is initialized (seeded) using srand()) when Cortex starts. Cortex uses the time() function to seed it. The function random() is controlled by rand2(), but rand2() gets the same seed every time Cortex is started or the persistent variables are re-initialized. If you don't properly seed rand2(), you may have problems. Here are some rules to follow:

  • Seed rand() and rand2() even if you don't think you are using them. Cortex uses them.
  • Do not use the random() function without seeding rand2().
  • Beware of random(), it omits the last value in a range: a=random(50,100) will give you a number from 50 to 99, not 100.
  • Do not seed every trial in your state file. See each generator only once (except for the next rule).
  • Seed both rand() and rand2() after clearing all persistent variables.
  • Do not seed with the same value every time you run Cortex.
  • Here is the proper way to seed rand2() with the time function:
    long now_time;
    time(&now_time);
    srand2(now_time);
  • Cortex automatically seeds rand() with the time() function. This means that the seed is repeated every 9.1 hours. (Usually not a problem).
  • Remember, rand() and rand2() are for integers, not floating point.

Cortex does not support unsigned integers

Cortex does not support unsigned integers. That can be a big headache under certain circumstances. For example, it is hard to write binary data to a disk file as a 16 bit unsigned value. Here is a subroutine that will take a (positive) long integer and cast it to a Cortex integer as if it was unsigned:

// ----------------- long2uint() ---------------------------------
// Assign long int value to signed int, but do the copy as if the
// signed int was an unsigned int. Cortex does not recognize unsigned
// data types, so this trick is done to management the sign bit (bit 15).
//
// The long integer must be less than 0xFFFF (MAX_UINT).
int long2uint(long v) {
   long dword;
   int u;

   dword=v;
   if (dword < 0x8000) u=dword; // simple case, positive int
   else {
      dword=dword-0x8000;       // make it a positive int
      u=dword;                  // copy to int
      u= u | 0x8000;            // force sign bit
   }
   return u;
} // long2uint()
 


Cortex trick: create a text data file with Cortex

Cortex makes an excellent data collection computer program for pure behavioral studies.  These types of studies do not benefit from the standard Cortex data files, however.  Researchers generally prefer a descriptive text data file that documents performance during a task. Here is a subroutine we use to create a text file during each trial.  This subroutine writes one line to the data file.

// ----------------------------------------------------
// -------      report_trial_results()       ----------
// ----------------------------------------------------
//
// Write text data to a file.  The file created is the data file 
// for behavioral experiments.  report_trial_results() builds the 
// file name from scratch each time it is called,
// then writes one line of text to the file.  
// Uses Append DOS mode, so nothing ever gets overwritten.
//    use:   report_trial_results(report_string)
//
//     report      a pointer to one line string that goes into file
//                 do not include <cr> in the string. 
//
void report_trial_results(pchar report) {
   char str_buf[40];
   char sn3[4],t_dir[40],t_file_name[80];
   char prefix[40];
   plong fhandle;   

   // Our text data files follow this naming scheme:
   //  .\TASKNAME\SSSTTNN.TXT
   //   TASKNAME  is a directory named after the behavioral task
   //   SSS       are the first 3 letters of the subject's name
   //   TT        is a 2 letter designation of the task
   //   NN        is the Cortex file extension number, e.g. .1,.2,.3, etc.
   //   TXT       the file name always ends with .TXT
   // subject_name[] is a global variable with the subject's name

   sn3[0]=subject_name[0];    // Create a null terminated, 3 character 
   sn3[1]=subject_name[1];    // version of subject name
   sn3[2]=subject_name[2];
   sn3[3]=NULL;
   strcpy(t_dir,task_name);         // directory name is task name
   append_backslash(t_dir);         // followed by a backslash

   // Build File Name
   strcpy(prefix,t_dir);       // directory
   strcat(prefix,sn3);         // add 3 char subject name
   strcat(prefix,task_name);   // add 2 char task name
   sprintf(t_file_name,"%s%03d.txt",prefix,ext_value);  // add file number
   fhandle = fopen(t_file_name,"a+");   // open that file

//   Mprintf(2,"%s",t_file_name);     // ****** debugging information
//   Mprintf(3,"%s",report);
//   MessageInt(4,fhandle);
   fprintf(fhandle,"%s\n",report);   // write data to file
   fclose(fhandle);
}

 

 

Here is an example of how we use the subroutine:

char report_string[100];



// Report string columns:
// col 1   T
// col 2   overall trial number
// col 3   valid trial number
// col 4   condition number
// col 5   location of correct stimulus
// col 6   1 or 0  (correct or incorrect)
// col 7   rewards

  sprintf(report_string,"T %4d %4d %3d %c %d %3d",file_trial_number,
  valid_trial_count,which_condition+1,location_cr,got_reward,rewarded_trials);
// col 8  task name
// col 9  condition name

  strcat(report_string," ");
  strcat(report_string,task_name);
report_trial_results(report_string);    // write this line to the text data file

 


 

Using a trace file to debug Cortex state files

Cortex crashes and it takes the operating system with it. VCortex will crash Windows XP just as fast as DOS Cortex will crash DOS. The question is, how do you debug a system that crashes? The answer is a trace file that is opened and closed each time something is written. That way, the file contents are not lost when the system crashes. Here is how:

// Global stuff
#define trace_file_name   "c:\\tracefile.txt"    // double backslash is needed inside of a c string
#define TRACE 1                                  // 1 to trace, 0 no trace file
#define trace_time     _long2                    // for TRACE option
char trace_string[50];
int trace(pchar report_string);


main() {
   
   if (get_trial_num()== 1) time(&trace_time);   // record the time when Cortex was first started

   if (TRACE) trace("START OF TRIAL");      // example use of trace function using only a string


   if (TRACE) {                            // example that uses a mix of strings and numbers
      sprintf(trace_string,"Retrial %d for problem %d ",retrial_number,problem_number);
      trace(trace_string);
  }

} // end of main()

// Write to trace file. Writes 1 line. prepends the time since last restart
// Opens and closes file every time (to survive a crash)
int trace(pchar report_string) {
   FILE tfh;                     // in DOS Cortex you may need: #define FILE plong
   int chars_out;
   long ttime,sec,ms,minutes;
   float tt;

   time(&ttime);                     // read clock (ms)
   tt=ttime;
   tt=tt/1000;
   ttime=ttime-trace_time;           // subtract time since last restart
   sec=ttime/1000;                   // total seconds
   ms=ttime-(sec*1000);              // remaining milliseconds
   minutes=sec/60;                   // number of minutes
   sec=sec-(minutes*60);             // remaining seconds
   tfh=fopen(trace_file_name,"at");  // open file for append
   if (tfh == NULL) return 0;        // could not get an handle
   chars_out=fprintf(tfh,"%04d:%02d %03d %s\n",minutes,sec,ms,report_string);
   // chars_out=fprintf(tfh,"%f %s\n",tt,report_string);
   if (fclose(tfh)) return 0;        // file close error
   if (chars_out < 0) return 0;      // file write error
   return 1;
} // trace()