From 6b6e581e5a191411b4ef17c669142adb9d35c483 Mon Sep 17 00:00:00 2001 From: adammorrissirrommada Date: Mon, 9 Dec 2019 14:28:04 +1100 Subject: [PATCH] Possible fix for https://github.com/plone/Products.CMFPlone/issues/391. Adaptive values were being updated in afterTrial(), making get(...,'atTrialTime',Inf) return the wrong (counterintuitive) value for the trial. In this version, the criterion correct/incorrect function is evaluated in afterTrial(), but update() is not called until beforeTrial() in the next trial. --- +neurostim/+plugins/adaptive.m | 34 +++++++++++++++++-------- +neurostim/+plugins/jitter.m | 2 +- +neurostim/+plugins/nDown1UpStaircase.m | 9 +++---- +neurostim/+plugins/psyBayes.m | 10 +++----- +neurostim/+plugins/quest.m | 14 +++------- demos/adaptiveDemo.m | 2 +- 6 files changed, 36 insertions(+), 35 deletions(-) diff --git a/+neurostim/+plugins/adaptive.m b/+neurostim/+plugins/adaptive.m index ab127375..e741cb44 100644 --- a/+neurostim/+plugins/adaptive.m +++ b/+neurostim/+plugins/adaptive.m @@ -17,15 +17,18 @@ end methods (Abstract) - % update(s) should change the internal state of the adaptive object using the outcome of the current - % trial (result = TRUE/Correct or FALSE/Incorrect). - update(s,result); + % update(s) should change the internal state of the adaptive object + % using the value (o.lastValue) and outcome of the just-completed trial + % (i.e. o.lastOutcome = TRUE/Correct or FALSE/Incorrect). + update(s); % getAdaptValue returns the current parameter value from the adaptive algorithm. v= getAdaptValue(s); end properties (SetAccess=private) overruleValue = []; %used to manually set the adaptive parameter value (for the rest of current trial) to something other than that returned by the adaptive algorithm. Ensures update() is based on the actual tested value. + lastOutcome = []; %True/false, the outcome of the most recent trial + lastValue = []; %The value of the adaptive parameter at the time the outcome was evaluated end methods @@ -199,23 +202,24 @@ function overrule(x1,newValue) function afterTrial(o) % This is called after cic sends the AFTERTRIAL event % (in cic.run) - % An emply o.design means that the adaptive parameter was + % An empty o.design means that the adaptive parameter was % assigned directly to a plugin property (probably a jitter) % and not part of a design. These get updated after every % trial, irrespective of the current condition/design. % Adaptive parameters defined as part of a design only get % updated when "their" condition/design is the currently active % one. - if isempty(o.design) || (strcmpi(o.cic.design,o.design) && ismember(o.cic.condition,o.conditions))% Check that this adaptive belongs to the current condition if o.cic.trial > o.ignoreN - % Call the derived class function to update it - correct = o.trialOutcome; % Evaluate the function that the user provided. - if numel(correct)>1 - error(['Your ''correct'' function in the adaptive parameter ' o.name ' does not evaluate to true or false']); + % Evaluate the function that the user provided (returning correct/incorrect) and store it. + %We'll use it to update the adaptive value in beforeTrial() + o.lastOutcome = o.trialOutcome; + if numel(o.lastOutcome)>1 + error(['Your ''trialOutcome'' function in the adaptive parameter ' o.name ' does not evaluate to true or false']); end - update(o,correct); % Pass it to the derived class to update - o.overruleValue = []; + + %Also store the value used + o.lastValue = o.getValue; else % Ignoring this trial end @@ -223,6 +227,14 @@ function afterTrial(o) end function beforeTrial(o) + + if ~isempty(o.lastOutcome) + % Allow the derived class to update (based on lastValue and lastOutcome) + update(o); + o.lastOutcome = []; + o.lastValue = []; + end + %Reset the overruled value. o.overruleValue = []; end diff --git a/+neurostim/+plugins/jitter.m b/+neurostim/+plugins/jitter.m index 301a0e56..c11e29dc 100644 --- a/+neurostim/+plugins/jitter.m +++ b/+neurostim/+plugins/jitter.m @@ -57,7 +57,7 @@ update(o); % Call it once now to initialize. end - function update(o,~) + function update(o) % The abstract adaptive parent class requires that we implement this % This is called after each trial. Update the internal value. The second arg is the success of the current trial, irrelevant here. if isa(o.distribution,'function_handle') diff --git a/+neurostim/+plugins/nDown1UpStaircase.m b/+neurostim/+plugins/nDown1UpStaircase.m index f8efb30d..37e30da9 100644 --- a/+neurostim/+plugins/nDown1UpStaircase.m +++ b/+neurostim/+plugins/nDown1UpStaircase.m @@ -66,12 +66,11 @@ o.value = startValue; end - function update(o,correct) + function update(o) % calculate and return the updated property value - - % current value - v = o.getValue; - if correct + v = o.lastValue; + + if o.lastOutcome % increment correct count o.cnt = o.cnt + 1; if o.cnt >= o.n diff --git a/+neurostim/+plugins/psyBayes.m b/+neurostim/+plugins/psyBayes.m index fcdb1aeb..01aa87e3 100644 --- a/+neurostim/+plugins/psyBayes.m +++ b/+neurostim/+plugins/psyBayes.m @@ -97,14 +97,10 @@ end - function update(o,response) + function update(o) % The abstract adaptive parent class requires that we implement this - % This is called after each trial. Update the internal value. The second arg is the success of the current trial, as determined - % in the parent class, using the trialResullt function - % specified by the user when constructing an object of this - % class. - parmValue = getValue(o); % This is the value that we used previously - [~,o.psy] = psybayes(o.psy, o.method, o.vars,parmValue,response); % Call to update. + % This is called after each trial. Update the internal value. + [~,o.psy] = psybayes(o.psy, o.method, o.vars,o.lastValue,o.lastOutcome); % Call to update. end function v =getAdaptValue(o) diff --git a/+neurostim/+plugins/quest.m b/+neurostim/+plugins/quest.m index a01c9fd1..e00dc3a0 100644 --- a/+neurostim/+plugins/quest.m +++ b/+neurostim/+plugins/quest.m @@ -73,17 +73,11 @@ o.momentFun = funs{ismember({'QUANTILE','MEAN','MODE'},p.Results.pdfMoment)}; end - function update(o,correct) + function update(o) % The abstract adaptive parent class requires that we implement this - % This is called after each trial. Update the internal value. The second arg is the success of the current trial, as determined - % in the parent class, using the trialResullt function - % specified by the user when constructing an object of this - % class. - parmValue = o.getValue; % This is the value that was used previously - intensity = o.p2i(parmValue); % Converti it to Quest intensity - if ~isempty(correct) - o.Q=QuestUpdate(o.Q,intensity,correct); % Add the new datum . - end + % This is called after each trial. Update the internal value. + intensity = o.p2i(o.lastValue); % Converti it to Quest intensity + o.Q=QuestUpdate(o.Q,intensity,o.lastOutcome); % Add the new datum . end function v =getAdaptValue(o) diff --git a/demos/adaptiveDemo.m b/demos/adaptiveDemo.m index 982e9439..c2b20919 100644 --- a/demos/adaptiveDemo.m +++ b/demos/adaptiveDemo.m @@ -120,7 +120,7 @@ % Create a block for this design and specify the repeats per design myBlock=block('myBlock',d); -myBlock.nrRepeats = 10; % Because the design has 2 conditions, this results in 2*nrRepeats trials. +myBlock.nrRepeats = 50; % Because the design has 2 conditions, this results in 2*nrRepeats trials. c.run(myBlock); %% Do some analysis on the data