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