
% -------------------------------------------------------------------------
% Downloaded 10.08.2018 from:
% http://www.columbia.edu/~bsm2105/type2sdt/archive/index.html
% Author: Brian Maniscalco
% Contact: brian@psych.columbia.edu

% 10.08.2018: OKH added calculation for type 1 c to the results

% NOTE FROM WEBPAGE:
% If you use this analysis file, please reference the Consciousness &
% Cognition paper:
%       Maniscalco, B., & Lau, H. (2012). A signal detection 
%       theoretic approach for estimating metacognitive sensitivity from 
%       confidence ratings. Consciousness and Cognition, 21(1), 422–430. 
%       doi:10.1016/j.concog.2011.09.021)
% And the website:
%       http://www.columbia.edu/~bsm2105/type2sdt/archive/index.html
% -------------------------------------------------------------------------

function fit = tapas_fit_meta_d_MLE(nR_S1, nR_S2, s, fncdf, fninv)

% fit = fit_meta_d_MLE(nR_S1, nR_S2, s, fncdf, fninv)
%
% Given data from an experiment where an observer discriminates between two
% stimulus alternatives on every trial and provides confidence ratings,
% provides a type 2 SDT analysis of the data.
%
% INPUTS
%
% * nR_S1, nR_S2
% these are vectors containing the total number of responses in
% each response category, conditional on presentation of S1 and S2.
%
% e.g. if nR_S1 = [100 50 20 10 5 1], then when stimulus S1 was
% presented, the subject had the following response counts:
% responded S1, rating=3 : 100 times
% responded S1, rating=2 : 50 times
% responded S1, rating=1 : 20 times
% responded S2, rating=1 : 10 times
% responded S2, rating=2 : 5 times
% responded S2, rating=3 : 1 time
%
% The ordering of response / rating counts for S2 should be the same as it
% is for S1. e.g. if nR_S2 = [3 7 8 12 27 89], then when stimulus S2 was
% presented, the subject had the following response counts:
% responded S1, rating=3 : 3 times
% responded S1, rating=2 : 7 times
% responded S1, rating=1 : 8 times
% responded S2, rating=1 : 12 times
% responded S2, rating=2 : 27 times
% responded S2, rating=3 : 89 times
%
% N.B. if nR_S1 or nR_S2 contain zeros, this may interfere with estimation of
% meta-d'.
%
% Some options for dealing with response cell counts containing zeros are:
% 
% (1) Add a small adjustment factor, e.g. adj_f = 1/(length(nR_S1), to each 
% input vector:
% 
% adj_f = 1/length(nR_S1);
% nR_S1_adj = nR_S1 + adj_f;
% nR_S2_adj = nR_S2 + adj_f;
% 
% This is a generalization of the correction for similar estimation issues of
% type 1 d' as recommended in
% 
% Hautus, M. J. (1995). Corrections for extreme proportions and their biasing 
%     effects on estimated values of d'. Behavior Research Methods, Instruments, 
%     & Computers, 27, 46-51.
%     
% When using this correction method, it is recommended to add the adjustment 
% factor to ALL data for all subjects, even for those subjects whose data is 
% not in need of such correction, in order to avoid biases in the analysis 
% (cf Snodgrass & Corwin, 1988).
% 
% (2) Collapse across rating categories.
% 
% e.g. if your data set has 4 possible confidence ratings such that length(nR_S1)==8,
% defining new input vectors
% 
% nR_S1_new = [sum(nR_S1(1:2)), sum(nR_S1(3:4)), sum(nR_S1(5:6)), sum(nR_S1(7:8))];
% nR_S2_new = [sum(nR_S2(1:2)), sum(nR_S2(3:4)), sum(nR_S2(5:6)), sum(nR_S2(7:8))];
% 
% might be sufficient to eliminate zeros from the input without using an adjustment.
%
% * s
% this is the ratio of standard deviations for type 1 distributions, i.e.
%
% s = sd(S1) / sd(S2)
%
% if not specified, s is set to a default value of 1.
% For most purposes, we recommend setting s = 1. 
% See http://www.columbia.edu/~bsm2105/type2sdt for further discussion.
%
% * fncdf
% a function handle for the CDF of the type 1 distribution.
% if not specified, fncdf defaults to @normcdf (i.e. CDF for normal
% distribution)
%
% * fninv
% a function handle for the inverse CDF of the type 1 distribution.
% if not specified, fninv defaults to @norminv
%
% OUTPUT
%
% Output is packaged in the struct "fit." 
% In the following, let S1 and S2 represent the distributions of evidence 
% generated by stimulus classes S1 and S2.
% Then the fields of "fit" are as follows:
% 
% fit.d1        = mean(S2) - mean(S1), in room-mean-square(sd(S1),sd(S2)) units
% fit.c1        = type 1 criterion
% fit.s         = sd(S1) / sd(S2)
% fit.meta_d    = meta-d' in RMS units
% fit.M_diff    = meta_da - da
% fit.M_ratio   = meta_da / da
% fit.meta_c1   = type 1 criterion for meta-d' fit, RMS units
% fit.t2ca_rS1  = type 2 criteria of "S1" responses for meta-d' fit, RMS units
% fit.t2ca_rS2  = type 2 criteria of "S2" responses for meta-d' fit, RMS units
%
% fit.S1units   = contains same parameters in sd(S1) units.
%                 these may be of use since the data-fitting is conducted  
%                 using parameters specified in sd(S1) units.
% 
% fit.logL          = log likelihood of the data fit
%
% fit.est_HR2_rS1  = estimated (from meta-d' fit) type 2 hit rates for S1 responses
% fit.obs_HR2_rS1  = actual type 2 hit rates for S1 responses
% fit.est_FAR2_rS1 = estimated type 2 false alarm rates for S1 responses
% fit.obs_FAR2_rS1 = actual type 2 false alarm rates for S1 responses
% 
% fit.est_HR2_rS2  = estimated type 2 hit rates for S2 responses
% fit.obs_HR2_rS2  = actual type 2 hit rates for S2 responses
% fit.est_FAR2_rS2 = estimated type 2 false alarm rates for S2 responses
% fit.obs_FAR2_rS2 = actual type 2 false alarm rates for S2 responses
%
% If there are N ratings, then there will be N-1 type 2 hit rates and false
% alarm rates. 

% 2015/07/23 - fixed bug for output fit.meta_ca and fit.S1units.meta_c1. 
%            - added comments to help section as well as a warning output 
%              for nR_S1 or nR_S2 inputs containing zeros
% 2014/10/14 - updated discussion of "s" input in the help section above.
% 2010/09/07 - created


%% parse inputs

% check inputs
if ~mod(length(nR_S1),2)==0, error('input arrays must have an even number of elements'); end
if length(nR_S1)~=length(nR_S2), error('input arrays must have the same number of elements'); end

if any(nR_S1 == 0) || any(nR_S2 == 0)
    disp(' ')
    disp('WARNING!!')
    disp('---------')
    disp('Your inputs')
    disp(' ')
    disp(['nR_S1 = [' num2str(nR_S1) ']'])
    disp(['nR_S2 = [' num2str(nR_S2) ']'])
    disp(' ')
    disp('contain zeros! This may interfere with proper estimation of meta-d''.')
    disp('See ''help fit_meta_d_MLE'' for more information.')
    disp(' ')
    disp(' ')
end

% assign input default values
if ~exist('s','var') || isempty(s)
    s = 1;
end

if ~exist('fncdf','var') || isempty(fncdf)
    fncdf = @normcdf;
end

if ~exist('fninv','var') || isempty(fninv)
    fninv = @norminv;
end

nRatings = length(nR_S1) / 2;
nCriteria = 2*nRatings - 1;


%% set up constraints for MLE estimation

% parameters
% meta-d' - 1
% t2c     - nCriteria-1

A = [];
b = [];

% constrain type 2 criteria values,
% such that t2c(i) is always <= t2c(i+1)
% want t2c(i)   <= t2c(i+1) 
% -->  t2c(i+1) >= c(i) + 1e-5 (i.e. very small deviation from equality) 
% -->  t2c(i) - t2c(i+1) <= -1e-5 
for i = 2 : nCriteria-1

    A(end+1,[i i+1]) = [1 -1];
    b(end+1) = -1e-5;

end

% lower bounds on parameters
LB = [];
LB = [LB -10];                         % meta-d'
LB = [LB -20*ones(1,(nCriteria-1)/2)]; % criteria lower than t1c
LB = [LB zeros(1,(nCriteria-1)/2)];    % criteria higher than t1c

% upper bounds on parameters
UB = [];
UB = [UB 10];                          % meta-d'
UB = [UB zeros(1,(nCriteria-1)/2)];    % criteria lower than t1c
UB = [UB 20*ones(1,(nCriteria-1)/2)];  % criteria higher than t1c


%% select constant criterion type

constant_criterion = 'meta_d1 * (t1c1 / d1)'; % relative criterion


%% set up initial guess at parameter values

ratingHR  = [];
ratingFAR = [];
for c = 2:nRatings*2
    ratingHR(end+1) = sum(nR_S2(c:end)) / sum(nR_S2);
    ratingFAR(end+1) = sum(nR_S1(c:end)) / sum(nR_S1);
end

t1_index = nRatings;
t2_index = setdiff(1:2*nRatings-1, t1_index);

d1 = (1/s) * fninv( ratingHR(t1_index) ) - fninv( ratingFAR(t1_index) );
meta_d1 = d1;
c = -0.5 .* (fninv(ratingHR(t1_index)) + fninv(ratingFAR(t1_index)));
c1 = (-1/(1+s)) * ( fninv( ratingHR ) + fninv( ratingFAR ) );
t1c1 = c1(t1_index);
t2c1 = c1(t2_index);

guess = [meta_d1 t2c1 - eval(constant_criterion)];



%% find the best fit for type 2 hits and FAs

% save fit_meta_d_MLE.mat nR_S1 nR_S2 t2FAR_rS2 t2HR_rS2 t2FAR_rS1 t2HR_rS1 nRatings t1c1 s d1 fncdf constant_criterion
save fit_meta_d_MLE.mat nR_S1 nR_S2 nRatings d1 t1c1 s constant_criterion fncdf fninv

op = optimset(@fmincon);
op = optimset(op,'MaxFunEvals',100000);

[x f] = fmincon(@fit_meta_d_logL,guess,A,b,[],[],LB,UB,[],op);

meta_d1  = x(1);
t2c1     = x(2:end) + eval(constant_criterion);
logL     = -f;


%% data is fit, now to package it...

%% find observed t2FAR and t2HR 

% I_nR and C_nR are rating trial counts for incorrect and correct trials
% element i corresponds to # (in)correct w/ rating i
I_nR_rS2 = nR_S1(nRatings+1:end);
I_nR_rS1 = nR_S2(nRatings:-1:1);

C_nR_rS2 = nR_S2(nRatings+1:end);
C_nR_rS1 = nR_S1(nRatings:-1:1);

for i = 2:nRatings
    obs_FAR2_rS2(i-1) = sum( I_nR_rS2(i:end) ) / sum(I_nR_rS2);
    obs_HR2_rS2(i-1)  = sum( C_nR_rS2(i:end) ) / sum(C_nR_rS2);
    
    obs_FAR2_rS1(i-1) = sum( I_nR_rS1(i:end) ) / sum(I_nR_rS1);
    obs_HR2_rS1(i-1)  = sum( C_nR_rS1(i:end) ) / sum(C_nR_rS1);    
end


%% find estimated t2FAR and t2HR

S1mu = -meta_d1/2; S1sd = 1;
S2mu =  meta_d1/2; S2sd = S1sd/s;

mt1c1 = eval(constant_criterion);

C_area_rS2 = 1-fncdf(mt1c1,S2mu,S2sd);
I_area_rS2 = 1-fncdf(mt1c1,S1mu,S1sd);

C_area_rS1 = fncdf(mt1c1,S1mu,S1sd);
I_area_rS1 = fncdf(mt1c1,S2mu,S2sd);

for i=1:nRatings-1
    
    t2c1_lower = t2c1(nRatings-i);
    t2c1_upper = t2c1(nRatings-1+i);
        
    I_FAR_area_rS2 = 1-fncdf(t2c1_upper,S1mu,S1sd);
    C_HR_area_rS2  = 1-fncdf(t2c1_upper,S2mu,S2sd);

    I_FAR_area_rS1 = fncdf(t2c1_lower,S2mu,S2sd);
    C_HR_area_rS1  = fncdf(t2c1_lower,S1mu,S1sd);
    
    
    est_FAR2_rS2(i) = I_FAR_area_rS2 / I_area_rS2;
    est_HR2_rS2(i)  = C_HR_area_rS2 / C_area_rS2;

    est_FAR2_rS1(i) = I_FAR_area_rS1 / I_area_rS1;
    est_HR2_rS1(i)  = C_HR_area_rS1 / C_area_rS1;
    
end


%% package output

fit.d1        = sqrt(2/(1+s^2)) * s * d1;
fit.c1        = c;
fit.s         = s;
fit.meta_d    = sqrt(2/(1+s^2)) * s * meta_d1;
fit.M_diff    = fit.meta_d - fit.d1;
fit.M_ratio   = fit.meta_d / fit.d1;

mt1c1         = eval(constant_criterion);
fit.meta_c1   = ( sqrt(2).*s ./ sqrt(1+s.^2) ) .* mt1c1;

t2ca          = ( sqrt(2).*s ./ sqrt(1+s.^2) ) .* t2c1;
fit.t2ca_rS1  = t2ca(1:nRatings-1);
fit.t2ca_rS2  = t2ca(nRatings:end);

fit.S1units.d1        = d1;
fit.S1units.meta_d1   = meta_d1;
fit.S1units.s         = s;
fit.S1units.meta_c1   = mt1c1;
fit.S1units.t2c1_rS1  = t2c1(1:nRatings-1);
fit.S1units.t2c1_rS2  = t2c1(nRatings:end);

fit.logL           = logL;

fit.est_HR2_rS1  = est_HR2_rS1;
fit.obs_HR2_rS1  = obs_HR2_rS1;

fit.est_FAR2_rS1 = est_FAR2_rS1;
fit.obs_FAR2_rS1 = obs_FAR2_rS1;

fit.est_HR2_rS2  = est_HR2_rS2;
fit.obs_HR2_rS2  = obs_HR2_rS2;

fit.est_FAR2_rS2 = est_FAR2_rS2;
fit.obs_FAR2_rS2 = obs_FAR2_rS2;


%% clean up
delete fit_meta_d_MLE.mat

end


%% function to find the likelihood of parameter values, given observed data
function logL = fit_meta_d_logL(parameters)

% set up parameters
meta_d1  = parameters(1);
t2c1     = parameters(2:end);

% loads:
% nR_S1 nR_S2 nRatings d1 t1c1 s constant_criterion fncdf fninv
load fit_meta_d_MLE.mat


% define mean and SD of S1 and S2 distributions
S1mu = -meta_d1/2; S1sd = 1;
S2mu =  meta_d1/2; S2sd = S1sd/s;


% adjust so that the type 1 criterion is set at 0
% (this is just to work with optimization toolbox constraints...
%  to simplify defining the upper and lower bounds of type 2 criteria)
S1mu = S1mu - eval(constant_criterion);
S2mu = S2mu - eval(constant_criterion);

t1c1 = 0;



%%% set up MLE analysis

% get type 2 response counts
for i = 1:nRatings
    
    % S1 responses
    nC_rS1(i) = nR_S1(i);
    nI_rS1(i) = nR_S2(i);
    
    % S2 responses
    nC_rS2(i) = nR_S2(nRatings+i);
    nI_rS2(i) = nR_S1(nRatings+i);
    
end

% get type 2 probabilities
C_area_rS1 = fncdf(t1c1,S1mu,S1sd);
I_area_rS1 = fncdf(t1c1,S2mu,S2sd);

C_area_rS2 = 1-fncdf(t1c1,S2mu,S2sd);
I_area_rS2 = 1-fncdf(t1c1,S1mu,S1sd);

t2c1x = [-Inf t2c1(1:nRatings-1) t1c1 t2c1(nRatings:end) Inf];

for i = 1:nRatings
    prC_rS1(i) = ( fncdf(t2c1x(i+1),S1mu,S1sd) - fncdf(t2c1x(i),S1mu,S1sd) ) / C_area_rS1;
    prI_rS1(i) = ( fncdf(t2c1x(i+1),S2mu,S2sd) - fncdf(t2c1x(i),S2mu,S2sd) ) / I_area_rS1;
    
    prC_rS2(i) = ( (1-fncdf(t2c1x(nRatings+i),S2mu,S2sd)) - (1-fncdf(t2c1x(nRatings+i+1),S2mu,S2sd)) ) / C_area_rS2;
    prI_rS2(i) = ( (1-fncdf(t2c1x(nRatings+i),S1mu,S1sd)) - (1-fncdf(t2c1x(nRatings+i+1),S1mu,S1sd)) ) / I_area_rS2;
end
    

% calculate logL
logL = 0;
for i = 1:nRatings
    
    logL = logL + nC_rS1(i)*log(prC_rS1(i)) + nI_rS1(i)*log(prI_rS1(i)) + ...
                  nC_rS2(i)*log(prC_rS2(i)) + nI_rS2(i)*log(prI_rS2(i));
              
end

if isnan(logL), logL=-Inf; end

logL = -logL;
  
end