home *** CD-ROM | disk | FTP | other *** search
/ Tricks of the Mac Game Programming Gurus / TricksOfTheMacGameProgrammingGurus.iso / More Source / Libraries / VideoToolbox 95.04.18 / Utilities / Quick3 / Quick3.c < prev    next >
Encoding:
C/C++ Source or Header  |  1995-01-24  |  16.8 KB  |  405 lines  |  [TEXT/MMCC]

  1. /*
  2. Quick3.c 
  3. Copyright (c) 1990-1993 Denis G. Pelli 
  4. Quick3.c is a C program that makes maximum likelihood fits of simple models to
  5. psychometric data. This is a replacement for, and to some extent is derived from
  6. the old QUICK.FOR program written by A.B.Watson. See A.B.Watson (1979)
  7. Probability summation over time. Vision Research 19, 515-522.
  8.  
  9. The principal difference between this program and the original QUICK, is in how
  10. the goodness of fit is calculated. Both programs assess the goodness of fit by
  11. comparing the log likelihoods of the model fit (usually Weibull) with a null
  12. hypothesis. Minus two times this log likelihood ratio is approximately chi
  13. square, with a number of degrees of freedom equal to the difference in degrees
  14. of freedom of the two hypotheses. QUICK used an unconstrained psychometric
  15. function, which adopted the actual proportion correct at each contrast. Quick3
  16. uses a monotone hypothesis that is constrained to be monotonically increasing
  17. as a function of contrast. The virtue of the latter hypothesis is that it is
  18. easier to estimate its degrees of freedom when there are unequal numbers of data
  19. points at each contrast. (However, this estimate is seat-of-my-pants.) Finally,
  20. this program actually computes the significance for you, from the chi square
  21. value and the estimate of its degrees of freedom. This significance value is of
  22. some use in choosing a small fraction of your fits to discard (e.g. if
  23. significance is less than 0.05). This significance is misleadingly low when both
  24. hypotheses have nearly the same number of degrees of freedom (i.e. you only have
  25. a few contrasts). Looking at the significance is helpful, but not a substitute
  26. for thinking about whether your results are really reasonable.
  27.  
  28. Note that Quick3.c is just a front end, dealing with the user interface and
  29. reading and writing files. All the work is done by one call to
  30. PsychometricFit(). In many cases it will be convenient to call that routine
  31. directly from your program that collects (or generates) the data, rather than
  32. saving the data in a file for subsequent analysis by Quick3.c. If you want to
  33. use a psychometric model other than the Weibull function all you need to do is
  34. write a small function to implement it, based on Weibull.c. It should be easy.
  35. PsychometricFit() uses whatever psychometric function is supplied in the call.
  36.  
  37. Quick3 reads in your data from a text file and analyzes them. The results are
  38. shown on the screen, and are saved in two kinds of output file. The *.fit file is
  39. in Excel format and has just a minimal one-line summary for each condition,
  40. giving the parameters and goodness of fit. The *.plot file is in text format and
  41. is suitable for plotting (e.g. by CricketGraph) of the psychometric data along
  42. with the Weibull and Monotonic fits. For Macintosh users, the output files are
  43. TEXT files, but have the appropriate creator so that double-clicking them opens
  44. the data file in the appropriate application, either Excel or Cricket Graph.
  45. For maximum convenience, change the definition of PLOT_CREATOR to match that of 
  46. your favorite graphing program.
  47.  
  48. We no longer use CricketGraph, but, in case you do, we've included CricketGraph
  49. format files called "CricketGraph Quick3" (for the original CricketGraph) and
  50. "CA-CricketGraphIII Quick3" (for the new CricketGraphIII). Put the appropriate
  51. file in the same folder as the CricketGraph application. When CricketGraph
  52. starts up it will read in the format file, and later you'll be able to select
  53. that format from CricketGraph's Format menu when you plot your data.
  54.  
  55. Quick3's input file format is simple and flexible. It is a text file. Lines
  56. beginning with "#" are treated as comments. Comments are either copied directly
  57. to the .fit file, or used as a name for the next "condition" (i.e. block of
  58. data). The file must begin with at least one comment line. The last of several
  59. contiguous comment lines will be taken to be the name for the following
  60. condition. All the non-comment lines until the next comment will be interpreted
  61. as the data for that condition.
  62.  
  63. An experiment usually consists of several conditions, or blocks of trials.
  64. Within the condition the observer will be tested repeatedly at various
  65. contrasts. The results consist of the number of trials correct at each contrast.
  66. The results may be described by a set of "contrast records". Each contrast
  67. record consists of a contrast (e.g. 0.012), the number of trials at that
  68. contrast (e.g. 1 or 100), and the number of correct responses (e.g. 0 or 79).
  69. Quick3 expects a contrast record to look like this:
  70. 0.012 100 79
  71. The three numbers are all on one line, and separated by white space, so sscanf
  72. can parse them. Anything on the line after the third number is ignored. All
  73. contrast records until the next comment (line beginning with "#") or the end of
  74. file are assumed to be from the same condition and are analyzed together. The
  75. contrast records may be in any order. The records will be sorted into order of
  76. ascending contrast. You may have any number of trials at each contrast, up to
  77. about two billion (i.e. LONG_MAX in limits.h). It's okay to have multiple
  78. records with equal contrasts. The data will be merged, adding up the trials at
  79. that contrast. You can even be really lazy and just write out each trial as a
  80. separate contrast record, in whatever order, without keeping track of how many,
  81. etc.
  82.  
  83. You may add records with zero trials (and zero correct) so as to have fitted
  84. values computed at those contrasts in the .plot file.
  85.  
  86. Here's an example of a data file, suitable for analysis by Quick3:
  87. #This is a sample file. 
  88. #The first condition is called "monocular" and has 60 trials at 6 contrasts.
  89. #monocular
  90. 0.1 10 4
  91. 0.2 10 5
  92. 0.3 10 7
  93. 0.4 10 6 Anything after the specified data is ignored.
  94. 0.5 10 8
  95. 0.6 10 10
  96. # The "binocular" condition is bigger: 1000 trials.
  97. #binocular
  98.  0.056     100    53
  99.  0.064     100    53
  100.  0.073     100    65
  101.  0.083     100    77
  102.  0.094     100    81
  103.  0.107     100    84
  104.  0.121     100    96
  105.  0.138     100    99
  106.  0.156     100   100
  107.  0.178     100   100
  108. #all done
  109.  
  110. Here's the resulting .fit file (tabs will space properly in Excel, but not here):
  111. Condition    logAlpha    beta    gamma    delta    free params    signif.    Chi sq.    d.f.    trials    contrasts
  112. #This is a sample file. 
  113. #The first condition is called "monocular" and contains 60 trials at 6 contrasts.
  114. monocular    -0.305    8.269    0.524    0    4    0.183    1.773    1    60    6
  115. # The "binocular" condition is bigger: 1000 trials.
  116. binocular    -1.015    3.811    0.482    0    4    0.479    5.521    6    1000    10
  117. #all done                                        
  118.  
  119. SOURCES:
  120.  
  121. Quick3.h
  122. LogLikelihood.c
  123. MonotonicFit.c
  124. PsychometricFit.c
  125. Quick3.c
  126. SortAndMergeContrasts.c
  127. Weibull.c
  128. #From Denis Pelli's VideoToolbox:
  129. VideoToolbox.h
  130. Binomial.c
  131. ChiSquare.c
  132. Normal.c
  133. SetFileInfo.c        # Used only on the Macintosh
  134. #From Numerical Recipes in C:
  135. nr.h
  136. NRUTIL.h
  137. BRENT.C
  138. F1DIM.C
  139. LINMIN.C
  140. MNBRAK.C
  141. NRUTIL.C
  142. POWELL.C
  143.  
  144. HISTORY:
  145. 4/7/90        dgp    wrote it.
  146. 4/10/90        dgp    changed .fit format from %.3f to %.4f. Accept only printing characters
  147.                 in the condition name, ignoring everything after the first non-printing
  148.                 character. This deals with the fact that if an Excel file is used for
  149.                 input, there are lots of trailing tabs that should be ignored. Checked
  150.                 that files were actually open before closing 'em. Jeesh!
  151. 10/29/90    dgp    tidied up comments.
  152. 1/20/91        dgp    updated calls to BinomialUpperBound & BinomialUpperBound to
  153.                 conform to new definition.
  154. 8/24/91        dgp    Made compatible with THINK C 5.0.
  155. 11/17/92    dgp Now SetVol() after calling SFGetFile, so that file can be in any folder,
  156.                 not just the same folder that Quick3 is in.
  157. 1/19/93        dgp    put #ifs around the Mac-dependent code.
  158. 2/20/93        dgp    added call to Require().
  159. 8/5/93        dgp    moved call to Require() into a separate main(), since application
  160.                 failed on Mac Plus before it got to the call to Require(). Thanks
  161.                 to Robert Friedman friedman@cortex.health.ufl.edu for reporting the
  162.                 problem.
  163. 10/13/93    dgp    added comments about CricketGraphIII. Allow compile-time choice of
  164.                 Mac file's creator.
  165. 3/13/94        dgp    replaced MyFGets by standard fgets. Edited the code slightly, making
  166.                 it a bit more resilient of errors in data file format.
  167. 6/15/94        dgp    slightly enhanced the code that reads the data file, to accept, e.g.
  168.                 34.0, as an integer. Erik Herzog & Katey Burns had gotten very confused
  169.                 by Quick3's inability to read a data file produced by Excel that looked
  170.                 fine but was read by Quick3 as mostly zero, because it choked on the
  171.                 ".0" that Excel had added to all the integers.
  172. 6/16/94        dgp    Neither CA-CricketGraph III 1.52 (the current version) nor DeltaGraph 3
  173.                 will open a text file, of which they are the "creator", if you just
  174.                 double-clicking the text file. Kaleidagraph does. So the plot files
  175.                 are set to have Kaleidraph as the creator. Double clicking them will
  176.                 open them as data sheets in Kaleidagraph, where you can graph them.
  177. 7/29/94 dgp Eliminated use of "#s" printf format, since it's not supported by
  178.             Metrowerks CodeWarrior C.
  179. 9/5/94 dgp removed assumption in printf's that int==short.
  180. 11/1/94 dgp fixed minor bug in SortAndMergeContrasts reported by Bart Farell. If there
  181. were more than 2 identical contrasts, the 3rd would not be merged. This did not corrupt
  182. any data, it just produced a less compact printout than it should. It had no effect
  183. on Weibull fits since they effectively treat each trial independently, but it did
  184. allow an extra degree of freedom per unmerged contrast to the monotonic fit.
  185. Changed the "contrasts" field from "int" to "long".
  186. */
  187. //#define PLOT_CREATOR    'CGRF'    /* Macintosh Cricket Graph TEXT file */
  188. //#define PLOT_CREATOR    'CRGR'    /* Macintosh CA-Cricket Graph III TEXT file */
  189. #define PLOT_CREATOR    'QKPT'    /* Macintosh Kaleidagraph TEXT file */
  190. #include "VideoToolbox.h"
  191. #include "Quick3.h"
  192. #include "nr.h"                /* Numerical Recipes in C*/
  193. #include <assert.h>
  194. #if MACINTOSH
  195.     #include <QuickDraw.h>
  196.     #include <StandardFile.h>
  197. #endif
  198. void Quick3(void);
  199.  
  200. void main(void)
  201. {
  202.     #if MACINTOSH
  203.         Require(0);    // check for any required cpu and fpu.
  204.     #endif
  205.     Quick3();
  206. }
  207. void Quick3(void)
  208. {
  209.     static dataRecord data,monotonicData;
  210.     contrastRecord *cPtr;
  211.     static paramRecord params, initParams;
  212.     double *paramPtr=¶ms.logAlpha;    /* a generic way of accessing the parameters */
  213.     long i;
  214.     double chiSquare,modelLL,monotonicLL;    /* log likelihood */
  215.     int chiSquareDF,modelDF,monotonicDF;    /* degrees of freedom */
  216.     double significance;
  217.     FILE *dataFile=NULL,*fitFile=NULL,*plotFile=NULL;
  218.     char dataFileName[50],fitFileName[50],plotFileName[50],string[100],*s;
  219.     char conditionName[100];
  220.     long trials;
  221.     unsigned int a;
  222.     PsychometricFunctionPtr ModelFunction=&Weibull;    /* a psychometric function */
  223.     static const char modelName[]="Weibull";
  224.     static const char paramName[PARAMS][16]={"logAlpha","beta","gamma","delta"};
  225.     #if MACINTOSH
  226.         Point where;
  227.         static SFTypeList typeList;
  228.         static SFReply reply;
  229.         assert(StackSpace()>4000);
  230.     #endif
  231.  
  232.     MaximizeConsoleHeight();
  233.     printf("\n");    /* ask THINK C to init QuickDraw */
  234.  
  235.     /* initial values for the parameters of the psychometric function */
  236.     params.logAlpha=0.0;
  237.     params.beta=3.5;
  238.     params.gamma=0.5;
  239.     params.delta=0.01;
  240.     
  241.     #if MACINTOSH
  242.         where.h=100;
  243.         where.v=100;
  244.         typeList[0]='TEXT';
  245.         reply.version=0;
  246.         SFGetFile(where,"\p",NULL,1,typeList,NULL,&reply);
  247.         if(!reply.good)PrintfExit("Couldn't open file.\n");
  248.         SetVol(NULL,reply.vRefNum);    /* look in that folder */
  249.         strcpy(dataFileName,p2cstr(reply.fName));
  250.     #else
  251.         // Insert code here to get data file name into dataFileName.
  252.     #endif
  253.     dataFile=fopen(dataFileName,"r");
  254.     if(dataFile==NULL)
  255.         PrintfExit("\007Sorry, can't find file \"%s\".\007\n",dataFileName);
  256.     printf("Reading \"%s\"\n",dataFileName);
  257.     strcpy(string,dataFileName);
  258.     if(strstr(string,".data"))strcpy(strstr(string,".data"),"");
  259.     else if(strstr(string,".yes"))strcpy(strstr(string,".yes"),"");
  260.     if(strlen(string)>0){
  261.         sprintf(fitFileName,"%s.fit",string);
  262.         printf("Creating output files:\n%s\n%s.<condition name>.plot\n"
  263.             ,fitFileName,string);
  264.         fitFile=fopen(fitFileName,"w");
  265.         if(fitFile==NULL)
  266.             PrintfExit("Sorry, I can't create file \"%s\".\007\n",fitFileName);
  267.         #if MACINTOSH
  268.             SetFileInfo(fitFileName,'TEXT','XCEL');        /* Excel file */
  269.         #endif
  270.     }
  271.     modelDF=PARAMS;    /* number of parameters to be adjusted in fitting */
  272.     printf("How many parameters shall be adjustable? 0 to %d (%d):",(int)PARAMS,modelDF);
  273.     gets(string);
  274.     sscanf(string,"%d",&modelDF);
  275.     for(i=1;i<modelDF;i++){
  276.         printf("Initial value for %s? (%.2f):",paramName[i],paramPtr[i]);
  277.         gets(string);
  278.         sscanf(string,"%lf",¶mPtr[i]);
  279.     }
  280.     for(i=modelDF;i<PARAMS;i++){
  281.         printf("Fixed value for %s? (%.2f):",paramName[i],paramPtr[i]);
  282.         gets(string);
  283.         sscanf(string,"%lf",¶mPtr[i]);
  284.     }
  285.     initParams=params;
  286.     
  287.     printf("Reading %s\n",dataFileName);
  288.     if(fitFile)fprintf(fitFile,"Condition\t%s\t%s\t%s\t%s\tfree params"
  289.         "\tsignif.\tChi sq.\td.f.\ttrials\tcontrasts\n"
  290.         ,paramName[0],paramName[1],paramName[2],paramName[3]);
  291.     while(!feof(dataFile)){
  292.         // last comment line before data is used as a name for the condition
  293.         fgets(string,sizeof(string),dataFile);
  294.         printf("\n");
  295.         while(a=fgetc(dataFile),ungetc(a,dataFile),a=='#' || a==EOF){
  296.             printf("%s",string);                                /* echo comment */
  297.             if(fitFile)fprintf(fitFile,"%s",string);
  298.             fgets(string,sizeof(string),dataFile);
  299.             if(feof(dataFile))break;
  300.         }
  301.         /* get condition name. Strip leading # and any trailing junk */
  302.         strcpy(conditionName,&string[1]);
  303.         for(i=0;i<strlen(conditionName);i++)
  304.             if(!isprint(conditionName[i]))conditionName[i]=0;
  305.         printf("\n");
  306.         data.contrasts=0;
  307.         while(a=fgetc(dataFile),ungetc(a,dataFile),a!='#' && a!=EOF){    /* read data */
  308.             if(data.contrasts>=MAX_CONTRASTS){
  309.                 SortAndMergeContrasts(&data);
  310.                 if(data.contrasts>=MAX_CONTRASTS)
  311.                     PrintfExit("Quick3: can't handle more than %d different contrasts.\n"
  312.                         ,(int)MAX_CONTRASTS);
  313.             }
  314.             fgets(string,sizeof(string),dataFile);
  315.             i=data.contrasts;
  316.             if(0)sscanf(string,"%lf\t%ld\t%ld"
  317.                 ,&data.c[i].contrast,&data.c[i].trials,&data.c[i].correct);
  318.             else{
  319.                 // this code will accept 34.0 as an integer
  320.                 s=string;
  321.                 data.c[i].contrast=strtod(s,&s);
  322.                 data.c[i].trials=strtod(s,&s);
  323.                 data.c[i].correct=strtod(s,&s);
  324.             }
  325.             data.contrasts++;
  326.         }
  327.         if(data.contrasts==0){
  328.             // No data. Skip to next condition.
  329.             continue;
  330.         }
  331.         SortAndMergeContrasts(&data);
  332.         trials=0;
  333.         for(i=0;i<data.contrasts;i++)trials+=data.c[i].trials;    /* total trials */
  334.         params=initParams;    /* initial guess & fixed values */
  335.         if(modelDF>0)
  336.             params.logAlpha=log(data.c[data.contrasts/2].contrast)/log(10.0);    /* median contrast */
  337.         printf("%s\n",conditionName);
  338.         printf("\t%s   %s   %s   %s  contrasts trials signif. Chi sq.   d.f.\n",
  339.             paramName[0],paramName[1],paramName[2],paramName[3]);
  340.         printf("Guess:");
  341.         printf("\t%7.2f%8.1f%8.2f%8.2f\n",
  342.             paramPtr[0],paramPtr[1],paramPtr[2],paramPtr[3]);
  343.         significance=PsychometricFit(¶ms,ModelFunction,&data,&modelLL,modelDF,
  344.             &chiSquare,&chiSquareDF);
  345.         printf("Fit:");
  346.         printf("\t%7.2f%8.1f%8.2f%8.2f",
  347.             paramPtr[0],paramPtr[1],paramPtr[2],paramPtr[3]);
  348.         printf("%8ld%8ld",data.contrasts,trials);
  349.         printf("%8.2f%8.1f%8d\n",significance,chiSquare,chiSquareDF);
  350.         if(fitFile)fprintf(fitFile,"%s"
  351.                 "\t%7.4f\t%7.4f\t%7.4f\t%7.4f\t%7d"
  352.                 "\t%7.4f\t%7.4f\t%7d"
  353.                 "\t%7ld\t%7ld\n",
  354.                 conditionName,
  355.                 paramPtr[0],paramPtr[1],paramPtr[2],paramPtr[3],modelDF,
  356.                 significance,chiSquare,chiSquareDF,
  357.                 trials,data.contrasts);
  358.     
  359.         /* Now create plot file */
  360.         if(fitFile){
  361.             strcpy(plotFileName,fitFileName);
  362.             plotFileName[strlen(plotFileName)-strlen(".fit")]=0;    /* strip off the ".fit" */
  363.             sprintf(plotFileName,"%s.%s.plot",plotFileName,conditionName);
  364.             plotFile=fopen(plotFileName,"w");
  365.             if(plotFile==NULL){
  366.                 PrintfExit("Sorry, I can't create file \"%s\".\n\007",plotFileName);
  367.             }
  368.             #if MACINTOSH
  369.                 SetFileInfo(plotFileName,'TEXT',PLOT_CREATOR);    /* specify graphing program */
  370.                 if(PLOT_CREATOR=='CGRF')fprintf(plotFile,"*\n");
  371.             #endif
  372.             monotonicData=data;
  373.             MonotonicFit(&monotonicData,&monotonicLL,&monotonicDF);    /* overwrites data with fit */
  374.             fprintf(plotFile,"Contrast\t%s\tLower bound\tUpper bound\t%s fit\tMonotone fit\tTrials\tCorrect\tParameters\n",
  375.                 conditionName,modelName);
  376.             for(i=0;i<data.contrasts;i++){
  377.                 cPtr=&data.c[i];
  378.                 fprintf(plotFile,"%.3f",cPtr->contrast);
  379.                 if(cPtr->trials>0){
  380.                     fprintf(plotFile,"\t%.3f",cPtr->correct/(double)cPtr->trials);
  381.                     fprintf(plotFile,"\t%.3f\t%.3f",
  382.                         BinomialLowerBound(0.95,cPtr->correct,cPtr->trials),
  383.                         BinomialUpperBound(0.95,cPtr->correct,cPtr->trials));
  384.                 }
  385.                 else
  386.                     fprintf(plotFile,"\t\t\t");
  387.                 fprintf(plotFile,"\t%.3f",(*ModelFunction)(cPtr->contrast,¶ms));
  388.                 if(cPtr->trials>0)
  389.                     fprintf(plotFile,"\t%.3f\t%5ld\t%5ld",
  390.                         monotonicData.c[i].correct/(double)monotonicData.c[i].trials,
  391.                         cPtr->trials,cPtr->correct);
  392.                 else fprintf(plotFile,"\t\t\t");
  393.                 if(i<PARAMS)fprintf(plotFile,"\t%s=%.3f",paramName[i],paramPtr[i]);
  394.                 if(i==0)fprintf(plotFile,"\talpha=%.4f",pow(10.0,paramPtr[0]));
  395.                 if(i==PARAMS)fprintf(plotFile,"\tsignificance=%.3f",significance);
  396.                 fprintf(plotFile,"\n");
  397.             }
  398.             if(plotFile)fclose(plotFile);
  399.         }
  400.     }
  401.     if(fitFile)fclose(fitFile);
  402.     if(dataFile)fclose(dataFile);
  403. }
  404.  
  405.