mirror of
https://github.com/psychopy/psychojs.git
synced 2025-05-10 18:50:54 +00:00
NF: Simple Stair Handler
This commit is contained in:
parent
8a6209b302
commit
b366e6a872
@ -640,8 +640,8 @@ export class PsychoJS
|
|||||||
|
|
||||||
if (showOK)
|
if (showOK)
|
||||||
{
|
{
|
||||||
let text = "Thank you for your patience. ";
|
const defaultMsg = "Thank you for your patience. Goodbye!";
|
||||||
text += (typeof message !== "undefined") ? message : "Goodbye!";
|
const text = (typeof message !== "undefined") ? message : defaultMsg;
|
||||||
this._gui.dialog({
|
this._gui.dialog({
|
||||||
message: text,
|
message: text,
|
||||||
onOK: onTerminate
|
onOK: onTerminate
|
||||||
|
@ -1533,8 +1533,8 @@ export class ServerManager extends PsychObject
|
|||||||
_setupPreloadQueue()
|
_setupPreloadQueue()
|
||||||
{
|
{
|
||||||
const response = {
|
const response = {
|
||||||
origin: "ServerManager._setupPreloadQueue",
|
origin: "ServerManager.[preload]",
|
||||||
context: "when setting up a preload queue"
|
context: "when downloading resources"
|
||||||
};
|
};
|
||||||
|
|
||||||
this._preloadQueue = new createjs.LoadQueue(true, "", true);
|
this._preloadQueue = new createjs.LoadQueue(true, "", true);
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
import {TrialHandler} from "./TrialHandler.js";
|
import {TrialHandler} from "./TrialHandler.js";
|
||||||
import {QuestHandler} from "./QuestHandler.js";
|
import {QuestHandler} from "./QuestHandler.js";
|
||||||
|
import {StairHandler} from "./StairHandler.js";
|
||||||
import * as util from "../util/Util.js";
|
import * as util from "../util/Util.js";
|
||||||
import seedrandom from "seedrandom";
|
import seedrandom from "seedrandom";
|
||||||
|
|
||||||
@ -81,6 +82,8 @@ export class MultiStairHandler extends TrialHandler
|
|||||||
this._randomNumberGenerator = seedrandom();
|
this._randomNumberGenerator = seedrandom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._finished = false;
|
||||||
|
|
||||||
this._prepareStaircases();
|
this._prepareStaircases();
|
||||||
this._nextTrial();
|
this._nextTrial();
|
||||||
}
|
}
|
||||||
@ -107,11 +110,10 @@ export class MultiStairHandler extends TrialHandler
|
|||||||
return this._currentStaircase.getQuestValue();
|
return this._currentStaircase.getQuestValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO similar for simple staircase:
|
if (this._currentStaircase instanceof StairHandler)
|
||||||
// if (this._currentStaircase instanceof StaircaseHandler)
|
{
|
||||||
// {
|
return this._currentStaircase.getStairValue();
|
||||||
// return this._currentStaircase.getStairValue();
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -125,6 +127,8 @@ export class MultiStairHandler extends TrialHandler
|
|||||||
*/
|
*/
|
||||||
addResponse(response, value)
|
addResponse(response, value)
|
||||||
{
|
{
|
||||||
|
this._psychoJS.logger.debug(`response= ${response}`);
|
||||||
|
|
||||||
// check that response is either 0 or 1:
|
// check that response is either 0 or 1:
|
||||||
if (response !== 0 && response !== 1)
|
if (response !== 0 && response !== 1)
|
||||||
{
|
{
|
||||||
@ -162,12 +166,6 @@ export class MultiStairHandler extends TrialHandler
|
|||||||
throw "conditions should be a non empty array of objects";
|
throw "conditions should be a non empty array of objects";
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO this is temporary until we have implemented StairHandler:
|
|
||||||
if (this._stairType === MultiStairHandler.StaircaseType.SIMPLE)
|
|
||||||
{
|
|
||||||
throw "'simple' staircases are currently not supported";
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const condition of this._conditions)
|
for (const condition of this._conditions)
|
||||||
{
|
{
|
||||||
// each condition must be an object:
|
// each condition must be an object:
|
||||||
@ -191,6 +189,7 @@ export class MultiStairHandler extends TrialHandler
|
|||||||
{
|
{
|
||||||
throw "QUEST conditions must include a startValSd field";
|
throw "QUEST conditions must include a startValSd field";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
@ -234,14 +233,47 @@ export class MultiStairHandler extends TrialHandler
|
|||||||
args.nTrials = this._nTrials;
|
args.nTrials = this._nTrials;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inform the StairHandler that it is instantiated from a MultiStairHandler
|
||||||
|
// (and so there is no need to update the trial list there since it is updated here)
|
||||||
|
args.fromMultiStair = true;
|
||||||
|
|
||||||
|
// TODO extraArgs
|
||||||
|
|
||||||
handler = new QuestHandler(args);
|
handler = new QuestHandler(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// simple StairCase handler:
|
// simple StairCase handler:
|
||||||
if (this._stairType === MultiStairHandler.StaircaseType.SIMPLE)
|
else if (this._stairType === MultiStairHandler.StaircaseType.SIMPLE)
|
||||||
{
|
{
|
||||||
// TODO not supported just yet, an exception is raised in _validateConditions
|
const args = Object.assign({}, condition);
|
||||||
continue;
|
args.psychoJS = this._psychoJS;
|
||||||
|
args.varName = this._varName;
|
||||||
|
// label becomes name:
|
||||||
|
args.name = condition.label;
|
||||||
|
args.autoLog = this._autoLog;
|
||||||
|
if (typeof condition.nTrials === "undefined")
|
||||||
|
{
|
||||||
|
args.nTrials = this._nTrials;
|
||||||
|
}
|
||||||
|
|
||||||
|
// inform the StairHandler that it is instantiated from a MultiStairHandler
|
||||||
|
// (and so there is no need to update the trial list there since it is updated here)
|
||||||
|
args.fromMultiStair = true;
|
||||||
|
|
||||||
|
// gather all args above and beyond those expected by the StairHandler constructor
|
||||||
|
// in a separate "extraArgs" argument:
|
||||||
|
const extraArgs = {};
|
||||||
|
const stairHandlerConstructorArgs = ["label", "psychoJS", "varName", "startVal", "minVal", "maxVal", "nTrials", "nReversals", "nUp", "nDown", "applyInitialRule", "stepSizes", "stepType", "name", "autolog", "fromMultiStair", "extraArgs"];
|
||||||
|
for (const key in condition)
|
||||||
|
{
|
||||||
|
if (stairHandlerConstructorArgs.indexOf(key) === -1)
|
||||||
|
{
|
||||||
|
extraArgs[key] = condition[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args["extraArgs"] = extraArgs;
|
||||||
|
|
||||||
|
handler = new StairHandler(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._staircases.push(handler);
|
this._staircases.push(handler);
|
||||||
@ -267,6 +299,8 @@ export class MultiStairHandler extends TrialHandler
|
|||||||
*/
|
*/
|
||||||
_nextTrial()
|
_nextTrial()
|
||||||
{
|
{
|
||||||
|
this._psychoJS.logger.debug(`current staircase (before update)= ${this._currentStaircase}`);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// if the current pass is empty, get a new one:
|
// if the current pass is empty, get a new one:
|
||||||
@ -298,7 +332,6 @@ export class MultiStairHandler extends TrialHandler
|
|||||||
// pick the next staircase in the pass:
|
// pick the next staircase in the pass:
|
||||||
this._currentStaircase = this._currentPass.shift();
|
this._currentStaircase = this._currentPass.shift();
|
||||||
|
|
||||||
|
|
||||||
// test for termination:
|
// test for termination:
|
||||||
if (typeof this._currentStaircase === "undefined")
|
if (typeof this._currentStaircase === "undefined")
|
||||||
{
|
{
|
||||||
@ -325,11 +358,10 @@ export class MultiStairHandler extends TrialHandler
|
|||||||
{
|
{
|
||||||
value = this._currentStaircase.getQuestValue();
|
value = this._currentStaircase.getQuestValue();
|
||||||
}
|
}
|
||||||
// TODO add a test for simple staircase:
|
if (this._currentStaircase instanceof StairHandler)
|
||||||
// if (this._currentStaircase instanceof StaircaseHandler)
|
{
|
||||||
// {
|
value = this._currentStaircase.getStairValue();
|
||||||
// value = this._currentStaircase.getStairValue();
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
this._psychoJS.logger.debug(`selected staircase: ${this._currentStaircase.name}, estimated value for variable ${this._varName}: ${value}`);
|
this._psychoJS.logger.debug(`selected staircase: ${this._currentStaircase.name}, estimated value for variable ${this._varName}: ${value}`);
|
||||||
@ -342,8 +374,10 @@ export class MultiStairHandler extends TrialHandler
|
|||||||
{
|
{
|
||||||
this._trialList[t] = {
|
this._trialList[t] = {
|
||||||
[this._name+"."+this._varName]: value,
|
[this._name+"."+this._varName]: value,
|
||||||
[this._name+".intensity"]: value
|
[this._name+".intensity"]: value,
|
||||||
|
[this._varName]: value
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const attribute of this._currentStaircase._userAttributes)
|
for (const attribute of this._currentStaircase._userAttributes)
|
||||||
{
|
{
|
||||||
// "name" becomes "label" again:
|
// "name" becomes "label" again:
|
||||||
@ -357,6 +391,15 @@ export class MultiStairHandler extends TrialHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("@@@@", this._currentStaircase._extraArgs);
|
||||||
|
for (const arg in this._currentStaircase._extraArgs)
|
||||||
|
{
|
||||||
|
this._trialList[t][this._name+"."+arg] = this._currentStaircase._extraArgs[arg];
|
||||||
|
this._trialList[t][arg] = this._currentStaircase._extraArgs[arg];
|
||||||
|
}
|
||||||
|
|
||||||
|
this._psychoJS.logger.debug(`updated the trialList at index: ${t} to: ${JSON.stringify(this._trialList[t])}`);
|
||||||
|
|
||||||
if (typeof this._snapshots[t] !== "undefined")
|
if (typeof this._snapshots[t] !== "undefined")
|
||||||
{
|
{
|
||||||
let fieldName = /*this._name + "." +*/ this._varName;
|
let fieldName = /*this._name + "." +*/ this._varName;
|
||||||
@ -383,6 +426,7 @@ export class MultiStairHandler extends TrialHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
435
src/data/StairHandler.js
Normal file
435
src/data/StairHandler.js
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
/**
|
||||||
|
* Stair Handler
|
||||||
|
*
|
||||||
|
* @author Alain Pitiot
|
||||||
|
* @version 2022.2.3
|
||||||
|
* @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org)
|
||||||
|
* @license Distributed under the terms of the MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import {TrialHandler} from "./TrialHandler.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>A Trial Handler that implements the Quest algorithm for quick measurement of
|
||||||
|
psychophysical thresholds. QuestHandler relies on the [jsQuest]{@link https://github.com/kurokida/jsQUEST} library, a port of Prof Dennis Pelli's QUEST algorithm by [Daiichiro Kuroki]{@link https://github.com/kurokida}.</p>
|
||||||
|
*
|
||||||
|
* @extends TrialHandler
|
||||||
|
*/
|
||||||
|
export class StairHandler extends TrialHandler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @memberof module:data
|
||||||
|
* @param {Object} options - the handler options
|
||||||
|
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
||||||
|
* @param {string} options.varName - the name of the variable / intensity / contrast / threshold manipulated by QUEST
|
||||||
|
* @param {number} options.startVal - initial guess for the threshold
|
||||||
|
* @param {number} options.minVal - minimum value for the threshold
|
||||||
|
* @param {number} options.maxVal - maximum value for the threshold
|
||||||
|
* @param {number} options.nTrials - maximum number of trials
|
||||||
|
* @param {string} options.name - name of the handler
|
||||||
|
* @param {boolean} [options.autoLog= false] - whether or not to log
|
||||||
|
*/
|
||||||
|
constructor({
|
||||||
|
psychoJS,
|
||||||
|
varName,
|
||||||
|
startVal,
|
||||||
|
minVal,
|
||||||
|
maxVal,
|
||||||
|
nTrials,
|
||||||
|
nReversals,
|
||||||
|
nUp,
|
||||||
|
nDown,
|
||||||
|
applyInitialRule,
|
||||||
|
stepSizes,
|
||||||
|
stepType,
|
||||||
|
name,
|
||||||
|
autoLog,
|
||||||
|
fromMultiStair,
|
||||||
|
extraArgs
|
||||||
|
} = {})
|
||||||
|
{
|
||||||
|
super({
|
||||||
|
psychoJS,
|
||||||
|
name,
|
||||||
|
autoLog,
|
||||||
|
method: TrialHandler.Method.SEQUENTIAL,
|
||||||
|
trialList: Array(nTrials),
|
||||||
|
nReps: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
this._addAttribute("varName", varName);
|
||||||
|
|
||||||
|
this._addAttribute("startVal", startVal);
|
||||||
|
this._addAttribute("minVal", minVal, Number.MIN_VALUE);
|
||||||
|
this._addAttribute("maxVal", maxVal, Number.MAX_VALUE);
|
||||||
|
|
||||||
|
this._addAttribute("nTrials", nTrials);
|
||||||
|
|
||||||
|
this._addAttribute("nReversals", nReversals, null);
|
||||||
|
this._addAttribute("nUp", nUp, 1);
|
||||||
|
this._addAttribute("nDown", nDown, 3);
|
||||||
|
this._addAttribute("applyInitialRule", applyInitialRule, true);
|
||||||
|
|
||||||
|
this._addAttribute("stepType", Symbol.for(stepType), 0.5);
|
||||||
|
this._addAttribute("stepSizes", stepSizes, [4]);
|
||||||
|
|
||||||
|
this._addAttribute("fromMultiStair", fromMultiStair, false);
|
||||||
|
|
||||||
|
this._addAttribute("extraArgs", extraArgs);
|
||||||
|
|
||||||
|
// turn stepSizes into an array if it is not one already:
|
||||||
|
if (!Array.isArray(this._stepSizes))
|
||||||
|
{
|
||||||
|
this._stepSizes = [this._stepSizes];
|
||||||
|
}
|
||||||
|
|
||||||
|
this._variableStep = (this._stepSizes.length > 1);
|
||||||
|
this._currentStepSize = this._stepSizes[0];
|
||||||
|
|
||||||
|
// TODO update the variables, a la staircase.py :nReversals, stepSizes, etc.
|
||||||
|
|
||||||
|
// setup the stair's starting point:
|
||||||
|
this._stairValue = this._startVal;
|
||||||
|
this._data = [];
|
||||||
|
this._values = [];
|
||||||
|
this._correctCounter = 0;
|
||||||
|
this._reversalPoints = [];
|
||||||
|
this.reversalIntensities = [];
|
||||||
|
this._initialRule = false;
|
||||||
|
this._currentDirection = StairHandler.Direction.START;
|
||||||
|
|
||||||
|
// update the next undefined trial in the trial list, and the associated snapshot:
|
||||||
|
this._updateTrialList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a response and advance the staircase.
|
||||||
|
*
|
||||||
|
* @param{number} response - the response to the trial, must be either 0 (incorrect or
|
||||||
|
* non-detected) or 1 (correct or detected)
|
||||||
|
* @param{number | undefined} value - optional intensity / contrast / threshold
|
||||||
|
* @param{boolean} [doAddData = true] - whether to add the response as data to the
|
||||||
|
* experiment
|
||||||
|
*/
|
||||||
|
addResponse(response, value, doAddData = true)
|
||||||
|
{
|
||||||
|
this._psychoJS.logger.debug(`response= ${response}`);
|
||||||
|
|
||||||
|
// check that response is either 0 or 1:
|
||||||
|
if (response !== 0 && response !== 1)
|
||||||
|
{
|
||||||
|
throw {
|
||||||
|
origin: "StairHandler.addResponse",
|
||||||
|
context: "when adding a trial response",
|
||||||
|
error: `the response must be either 0 or 1, got: ${JSON.stringify(response)}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doAddData)
|
||||||
|
{
|
||||||
|
this._psychoJS.experiment.addData(this._name + '.response', response);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._data.push(response);
|
||||||
|
|
||||||
|
// replace the last value with this one, if need be:
|
||||||
|
if (typeof value !== "undefined")
|
||||||
|
{
|
||||||
|
this._values.pop();
|
||||||
|
this._values.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update correctCounter:
|
||||||
|
if (response === 1)
|
||||||
|
{
|
||||||
|
if ( (this._data.length > 1) && (this._data.at(-2) === response))
|
||||||
|
{
|
||||||
|
++ this._correctCounter;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// reset the counter:
|
||||||
|
this._correctCounter = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// incorrect response:
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ( (this._data.length > 1) && (this._data.at(-2) === response))
|
||||||
|
{
|
||||||
|
-- this._correctCounter;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// reset the counter:
|
||||||
|
this._correctCounter = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._finished)
|
||||||
|
{
|
||||||
|
this.next();
|
||||||
|
|
||||||
|
// estimate the next value
|
||||||
|
// (and update the trial list and snapshots):
|
||||||
|
this._estimateStairValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current value of the variable / contrast / threshold.
|
||||||
|
*
|
||||||
|
* @returns {number} the current value
|
||||||
|
*/
|
||||||
|
getStairValue()
|
||||||
|
{
|
||||||
|
return this._stairValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current value of the variable / contrast / threshold.
|
||||||
|
*
|
||||||
|
* <p>This is the getter associated to getStairValue.</p>
|
||||||
|
*
|
||||||
|
* @returns {number} the intensity of the current staircase, or undefined if the trial has ended
|
||||||
|
*/
|
||||||
|
get intensity()
|
||||||
|
{
|
||||||
|
return this.getStairValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate the next value, based on the current value, the counter of correct responses,
|
||||||
|
* and the current staircase direction.
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_estimateStairValue()
|
||||||
|
{
|
||||||
|
this._psychoJS.logger.debug(`stairValue before update= ${this._stairValue}, currentDirection= ${this._currentDirection.toString()}, correctCounter= ${this._correctCounter}`);
|
||||||
|
|
||||||
|
// default: no reversal, same direction as previous trial
|
||||||
|
let reverseDirection = false;
|
||||||
|
|
||||||
|
// if we are at the very start and the initial rule applies, apply the 1-down, 1-up rule:
|
||||||
|
if (this.reversalIntensities.length === 0 && this._applyInitialRule)
|
||||||
|
{
|
||||||
|
// if the last response was correct:
|
||||||
|
if (this._data.at(-1) === 1)
|
||||||
|
{
|
||||||
|
reverseDirection = (this._currentDirection === StairHandler.Direction.UP);
|
||||||
|
this._currentDirection = StairHandler.Direction.DOWN;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reverseDirection = (this._currentDirection === StairHandler.Direction.DOWN);
|
||||||
|
this._currentDirection = StairHandler.Direction.UP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// n correct response: time to go down:
|
||||||
|
else if (this._correctCounter >= this._nDown)
|
||||||
|
{
|
||||||
|
reverseDirection = (this._currentDirection === StairHandler.Direction.UP);
|
||||||
|
this._currentDirection = StairHandler.Direction.DOWN;
|
||||||
|
}
|
||||||
|
// n wrong responses, time to go up:
|
||||||
|
else if (this._correctCounter <= -this._nUp)
|
||||||
|
{
|
||||||
|
reverseDirection = (this._currentDirection === StairHandler.Direction.DOWN);
|
||||||
|
this._currentDirection = StairHandler.Direction.UP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reverseDirection)
|
||||||
|
{
|
||||||
|
this._reversalPoints.push(this.thisTrialN);
|
||||||
|
this._initialRule = (this.reversalIntensities.length === 0 && this._applyInitialRule);
|
||||||
|
this.reversalIntensities.push(this._values.at(-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// check whether we should finish the trial:
|
||||||
|
if (this.reversalIntensities.length >= this._nReversals && this._values.length >= this._nTrials)
|
||||||
|
{
|
||||||
|
this._finished = true;
|
||||||
|
|
||||||
|
// update the snapshots associated with the current trial in the trial list:
|
||||||
|
for (let t = 0; t < this._snapshots.length - 1; ++t)
|
||||||
|
{
|
||||||
|
// the current trial is the last defined one:
|
||||||
|
if (typeof this._trialList[t + 1] === "undefined")
|
||||||
|
{
|
||||||
|
this._snapshots[t].finished = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the step size, if need be:
|
||||||
|
if (reverseDirection && this._variableStep)
|
||||||
|
{
|
||||||
|
// if we have gone past the end of the step size array, we use the last one:
|
||||||
|
if (this.reversalIntensities.length >= this._stepSizes.length)
|
||||||
|
{
|
||||||
|
this._currentStepSize = this._stepSizes.at(-1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this._currentStepSize = this._stepSizes.at(this.reversalIntensities.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply the new step size:
|
||||||
|
if ( (this.reversalIntensities.length === 0 || this._initialRule) && this._applyInitialRule )
|
||||||
|
{
|
||||||
|
this._initialRule = false;
|
||||||
|
|
||||||
|
if (this._data.at(-1) === 1)
|
||||||
|
{
|
||||||
|
this._decreaseValue();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this._increaseValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// n correct: decrease the value
|
||||||
|
else if (this._correctCounter >= this._nDown)
|
||||||
|
{
|
||||||
|
this._decreaseValue();
|
||||||
|
}
|
||||||
|
// n wrong: increase the value
|
||||||
|
else if (this._correctCounter <= -this._nUp)
|
||||||
|
{
|
||||||
|
this._increaseValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._psychoJS.logger.debug(`estimated value for variable ${this._varName}: ${this._stairValue}`);
|
||||||
|
|
||||||
|
// update the next undefined trial in the trial list, and the associated snapshot:
|
||||||
|
this._updateTrialList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the next undefined trial in the trial list, and the associated snapshot.
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_updateTrialList()
|
||||||
|
{
|
||||||
|
// if this StairHandler was instantiated from a MultiStairHandler, we do not update the trial list here,
|
||||||
|
// since it is updated by the MultiStairHandler instead
|
||||||
|
if (this._fromMultiStair)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let t = 0; t < this._trialList.length; ++t)
|
||||||
|
{
|
||||||
|
if (typeof this._trialList[t] === "undefined")
|
||||||
|
{
|
||||||
|
this._trialList[t] = { [this._varName]: this._stairValue };
|
||||||
|
|
||||||
|
this._psychoJS.logger.debug(`updated the trialList at: ${t}: ${JSON.stringify(this._trialList[t])}`);
|
||||||
|
|
||||||
|
if (typeof this._snapshots[t] !== "undefined")
|
||||||
|
{
|
||||||
|
this._snapshots[t][this._varName] = this._stairValue;
|
||||||
|
this._snapshots[t].trialAttributes.push(this._varName);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increase the current value of the variable / contrast / threshold.
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_increaseValue()
|
||||||
|
{
|
||||||
|
this._psychoJS.logger.debug(`stepType= ${this._stepType.toString()}, currentStepSize= ${this._currentStepSize}, stairValue (before update)= ${this._stairValue}`);
|
||||||
|
|
||||||
|
this._correctCounter = 0;
|
||||||
|
|
||||||
|
switch (this._stepType)
|
||||||
|
{
|
||||||
|
case StairHandler.StepType.DB:
|
||||||
|
this._stairValue *= Math.pow(10.0, this._currentStepSize / 20.0);
|
||||||
|
break;
|
||||||
|
case StairHandler.StepType.LOG:
|
||||||
|
this._stairValue *= Math.pow(10.0, this._currentStepSize);
|
||||||
|
break;
|
||||||
|
case StairHandler.StepType.LINEAR:
|
||||||
|
default:
|
||||||
|
this._stairValue += this._currentStepSize;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we do not go beyond the maximum value:
|
||||||
|
if (this._stairValue > this._maxVal)
|
||||||
|
{
|
||||||
|
this._stairValue = this._maxVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrease the current value of the variable / contrast / threshold.
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_decreaseValue()
|
||||||
|
{
|
||||||
|
this._psychoJS.logger.debug(`stepType= ${this._stepType.toString()}, currentStepSize= ${this._currentStepSize}, stairValue (before update)= ${this._stairValue}`);
|
||||||
|
|
||||||
|
this._correctCounter = 0;
|
||||||
|
|
||||||
|
switch (this._stepType)
|
||||||
|
{
|
||||||
|
case StairHandler.StepType.DB:
|
||||||
|
this._stairValue /= Math.pow(10.0, this._currentStepSize / 20.0);
|
||||||
|
break;
|
||||||
|
case StairHandler.StepType.LOG:
|
||||||
|
this._stairValue /= Math.pow(10.0, this._currentStepSize);
|
||||||
|
break;
|
||||||
|
case StairHandler.StepType.LINEAR:
|
||||||
|
default:
|
||||||
|
this._stairValue -= this._currentStepSize;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we do not go beyond the minimum value:
|
||||||
|
if (this._stairValue < this._minVal)
|
||||||
|
{
|
||||||
|
this._stairValue = this._minVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StairHandler step type
|
||||||
|
*
|
||||||
|
* @enum {Symbol}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
StairHandler.StepType = {
|
||||||
|
DB: Symbol.for("db"),
|
||||||
|
LINEAR: Symbol.for("lin"),
|
||||||
|
LOG: Symbol.for("log")
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StairHandler step direction.
|
||||||
|
*
|
||||||
|
* @enum {Symbol}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
StairHandler.Direction = {
|
||||||
|
START: Symbol.for("START"),
|
||||||
|
UP: Symbol.for("UP"),
|
||||||
|
DOWN: Symbol.for("DOWN")
|
||||||
|
};
|
@ -1,5 +1,6 @@
|
|||||||
export * from "./ExperimentHandler.js";
|
export * from "./ExperimentHandler.js";
|
||||||
export * from "./TrialHandler.js";
|
export * from "./TrialHandler.js";
|
||||||
export * from "./QuestHandler.js";
|
export * from "./QuestHandler.js";
|
||||||
|
export * from "./StairHandler.js";
|
||||||
export * from "./MultiStairHandler.js";
|
export * from "./MultiStairHandler.js";
|
||||||
export * from "./Shelf.js";
|
export * from "./Shelf.js";
|
||||||
|
@ -114,11 +114,6 @@ export class ButtonStim extends TextBox
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
this._addAttribute(
|
|
||||||
"numClicks",
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this._autoLog)
|
if (this._autoLog)
|
||||||
{
|
{
|
||||||
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${util.toString(this)}`);
|
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${util.toString(this)}`);
|
||||||
@ -144,4 +139,19 @@ export class ButtonStim extends TextBox
|
|||||||
{
|
{
|
||||||
return this.listener.isPressedIn(this, [1, 0, 0]);
|
return this.listener.isPressedIn(this, [1, 0, 0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the previously stored times on and times off.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
reset()
|
||||||
|
{
|
||||||
|
this.wasClicked = this.isClicked;
|
||||||
|
|
||||||
|
this.timesOn = [];
|
||||||
|
this.timesOff = [];
|
||||||
|
|
||||||
|
super.reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,7 @@ export class Survey extends VisualStim
|
|||||||
|
|
||||||
this._overallSurveyResults = {};
|
this._overallSurveyResults = {};
|
||||||
this._surveyData = undefined;
|
this._surveyData = undefined;
|
||||||
this._surveyModel = undefined;
|
this._surveyJSModel = undefined;
|
||||||
this._expressionsRunner = undefined;
|
this._expressionsRunner = undefined;
|
||||||
this._lastPageSwitchHandledIdx = -1;
|
this._lastPageSwitchHandledIdx = -1;
|
||||||
this._variables = {};
|
this._variables = {};
|
||||||
@ -228,6 +228,10 @@ export class Survey extends VisualStim
|
|||||||
model.surveyFlow.isRootNode = true;
|
model.surveyFlow.isRootNode = true;
|
||||||
|
|
||||||
this._surveyData = model;
|
this._surveyData = model;
|
||||||
|
|
||||||
|
// augment the question names with block names:
|
||||||
|
this._augmentQuestionNames();
|
||||||
|
|
||||||
this._setAttribute("model", model, log);
|
this._setAttribute("model", model, log);
|
||||||
this._onChange(true, true)();
|
this._onChange(true, true)();
|
||||||
}
|
}
|
||||||
@ -297,7 +301,7 @@ export class Survey extends VisualStim
|
|||||||
*/
|
*/
|
||||||
evaluateExpression(expression)
|
evaluateExpression(expression)
|
||||||
{
|
{
|
||||||
if (typeof expression === "undefined" || typeof this._surveyModel === "undefined")
|
if (typeof expression === "undefined" || typeof this._surveyJSModel === "undefined")
|
||||||
{
|
{
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -309,7 +313,7 @@ export class Survey extends VisualStim
|
|||||||
expression = `'${expression}'`;
|
expression = `'${expression}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._surveyModel.runExpression(expression);
|
return this._surveyJSModel.runExpression(expression);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -910,16 +914,19 @@ export class Survey extends VisualStim
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback triggered when the participant is done with the survey, i.e. when the
|
* Callback triggered when the participant has completed a SurveyJS Question Block.
|
||||||
* [Complete] button as been pressed.
|
|
||||||
*
|
*
|
||||||
* @param surveyModel
|
* @param {Object} node - super-flow QUESTION_BLOCK node
|
||||||
* @param options
|
* @param surveyModel - the associated SurveyJS model
|
||||||
|
* @param options - the SurveyJS model options
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_onSurveyComplete(surveyModel, options)
|
_onSurveyJSComplete(node, surveyModel, options)
|
||||||
{
|
{
|
||||||
|
// note: we need to add the node title to the responses
|
||||||
Object.assign(this._overallSurveyResults, surveyModel.data);
|
Object.assign(this._overallSurveyResults, surveyModel.data);
|
||||||
|
|
||||||
|
|
||||||
let completionCode = Survey.SURVEY_COMPLETION_CODES.NORMAL;
|
let completionCode = Survey.SURVEY_COMPLETION_CODES.NORMAL;
|
||||||
const questions = surveyModel.getAllQuestions();
|
const questions = surveyModel.getAllQuestions();
|
||||||
|
|
||||||
@ -947,7 +954,7 @@ export class Survey extends VisualStim
|
|||||||
surveyModel.stopTimer();
|
surveyModel.stopTimer();
|
||||||
|
|
||||||
// check whether the survey was completed:
|
// check whether the survey was completed:
|
||||||
const surveyVisibleQuestions = this._surveyModel.getAllQuestions(true);
|
const surveyVisibleQuestions = this._surveyJSModel.getAllQuestions(true);
|
||||||
const nbAnsweredQuestions = surveyVisibleQuestions.reduce(
|
const nbAnsweredQuestions = surveyVisibleQuestions.reduce(
|
||||||
(count, question) =>
|
(count, question) =>
|
||||||
{
|
{
|
||||||
@ -996,51 +1003,49 @@ export class Survey extends VisualStim
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the survey using flow data provided. This method runs recursively.
|
* Run a QUESTION_BLOCK as a SurveyJS survey.
|
||||||
*
|
*
|
||||||
|
* @param {Object} node - super-flow QUESTION_BLOCK node
|
||||||
|
* @param {Object} surveyData - the complete surveyData (model)
|
||||||
* @protected
|
* @protected
|
||||||
* @param {Object} surveyData - surveyData / model.
|
|
||||||
* @param {Object} surveyFlowBlock - XXX
|
|
||||||
* @return {void}
|
|
||||||
*/
|
*/
|
||||||
_beginSurvey(surveyData, surveyFlowBlock)
|
_runQuestionBlock(node, surveyData)
|
||||||
{
|
{
|
||||||
this._lastPageSwitchHandledIdx = -1;
|
this._lastPageSwitchHandledIdx = -1;
|
||||||
const surveyIdx = surveyFlowBlock.surveyIdx;
|
let surveyModelInput = this._processSurveyData(surveyData, node.surveyIdx);
|
||||||
let surveyModelInput = this._processSurveyData(surveyData, surveyIdx);
|
|
||||||
|
|
||||||
this._surveyModel = new window.Survey.Model(surveyModelInput);
|
this._surveyJSModel = new window.Survey.Model(surveyModelInput);
|
||||||
for (let j in this._variables)
|
for (let j in this._variables)
|
||||||
{
|
{
|
||||||
// Adding variables directly to hash to get higher performance (this is instantaneous compared to .setVariable()).
|
// Adding variables directly to hash to get higher performance (this is instantaneous compared to .setVariable()).
|
||||||
// At this stage we don't care to trigger all the callbacks like .setVariable() does, since this is very beginning of survey presentation.
|
// At this stage we don't care to trigger all the callbacks like .setVariable() does, since this is very beginning of survey presentation.
|
||||||
this._surveyModel.variablesHash[j] = this._variables[j];
|
this._surveyJSModel.variablesHash[j] = this._variables[j];
|
||||||
// this._surveyModel.setVariable(j, this._variables[j]);
|
// this._surveyModel.setVariable(j, this._variables[j]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._surveyModel.isInitialized)
|
if (!this._surveyJSModel.isInitialized)
|
||||||
{
|
{
|
||||||
this._registerCustomComponentCallbacks(this._surveyModel);
|
this._registerCustomComponentCallbacks(this._surveyJSModel);
|
||||||
this._surveyModel.onValueChanged.add(this._onQuestionValueChanged.bind(this));
|
this._surveyJSModel.onValueChanged.add(this._onQuestionValueChanged.bind(this));
|
||||||
this._surveyModel.onCurrentPageChanging.add(this._onCurrentPageChanging.bind(this));
|
this._surveyJSModel.onCurrentPageChanging.add(this._onCurrentPageChanging.bind(this));
|
||||||
this._surveyModel.onComplete.add(this._onSurveyComplete.bind(this));
|
this._surveyJSModel.onComplete.add( (surveyJSModel, options) => this._onSurveyJSComplete(node, surveyJSModel, options) );
|
||||||
this._surveyModel.onTextMarkdown.add(this._onTextMarkdown.bind(this));
|
this._surveyJSModel.onTextMarkdown.add(this._onTextMarkdown.bind(this));
|
||||||
this._surveyModel.isInitialized = true;
|
this._surveyJSModel.isInitialized = true;
|
||||||
this._surveyModel.onAfterRenderQuestion.add(this._handleAfterQuestionRender.bind(this));
|
this._surveyJSModel.onAfterRenderQuestion.add(this._handleAfterQuestionRender.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
const completeText = surveyIdx < this._surveyData.surveys.length - 1 ? (this._surveyModel.pageNextText || Survey.CAPTIONS.NEXT) : undefined;
|
const completeText = node.surveyIdx < this._surveyData.surveys.length - 1 ? (this._surveyJSModel.pageNextText || Survey.CAPTIONS.NEXT) : undefined;
|
||||||
jQuery(".survey").Survey({
|
jQuery(".survey").Survey({
|
||||||
model: this._surveyModel,
|
model: this._surveyJSModel,
|
||||||
showItemsInOrder: "column",
|
showItemsInOrder: "column",
|
||||||
completeText,
|
completeText,
|
||||||
...surveyData.surveySettings,
|
...surveyData.surveySettings
|
||||||
});
|
});
|
||||||
|
|
||||||
this._questionAnswerTimestampClock.reset();
|
this._questionAnswerTimestampClock.reset();
|
||||||
|
|
||||||
// TODO: should this be conditional?
|
// TODO: should this be conditional?
|
||||||
this._surveyModel.startTimer();
|
this._surveyJSModel.startTimer();
|
||||||
|
|
||||||
this._surveyRunningPromise = new Promise((res, rej) => {
|
this._surveyRunningPromise = new Promise((res, rej) => {
|
||||||
this._surveyRunningPromiseResolve = res;
|
this._surveyRunningPromiseResolve = res;
|
||||||
@ -1110,18 +1115,19 @@ export class Survey extends VisualStim
|
|||||||
|
|
||||||
else if (node.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.ENDSURVEY)
|
else if (node.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.ENDSURVEY)
|
||||||
{
|
{
|
||||||
if (this._surveyModel)
|
if (this._surveyJSModel)
|
||||||
{
|
{
|
||||||
this._surveyModel.setCompleted();
|
this._surveyJSModel.setCompleted();
|
||||||
}
|
}
|
||||||
console.log("EndSurvey block encountered, exiting.");
|
console.log("EndSurvey block encountered, exiting.");
|
||||||
nodeExitCode = Survey.NODE_EXIT_CODES.BREAK_FLOW;
|
nodeExitCode = Survey.NODE_EXIT_CODES.BREAK_FLOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QUESTION_BLOCK:
|
||||||
else if (node.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.DIRECT)
|
else if (node.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.DIRECT)
|
||||||
{
|
{
|
||||||
const surveyCompletionCode = await this._beginSurvey(surveyData, node);
|
const surveyCompletionCode = await this._runQuestionBlock(node, surveyData);
|
||||||
Object.assign({}, prevBlockResults, this._surveyModel.data);
|
Object.assign({}, prevBlockResults, this._surveyJSModel.data);
|
||||||
|
|
||||||
// SkipLogic had destination set to ENDOFSURVEY.
|
// SkipLogic had destination set to ENDOFSURVEY.
|
||||||
if (surveyCompletionCode === Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY)
|
if (surveyCompletionCode === Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY)
|
||||||
@ -1161,7 +1167,7 @@ export class Survey extends VisualStim
|
|||||||
|
|
||||||
_handleWindowResize(e)
|
_handleWindowResize(e)
|
||||||
{
|
{
|
||||||
if (this._surveyModel)
|
if (this._surveyJSModel)
|
||||||
{
|
{
|
||||||
for (let i = this._signaturePads.length - 1; i >= 0; i--)
|
for (let i = this._signaturePads.length - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
@ -1227,4 +1233,31 @@ export class Survey extends VisualStim
|
|||||||
// TODO
|
// TODO
|
||||||
// util.loadCss("./survey/css/grey_style.css");
|
// util.loadCss("./survey/css/grey_style.css");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Augment the model question names with model names.
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_augmentQuestionNames()
|
||||||
|
{
|
||||||
|
if (!("surveys" in this._surveyData))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const surveys = this._surveyData["surveys"];
|
||||||
|
for (const survey of surveys)
|
||||||
|
{
|
||||||
|
if (!("title" in survey) || !("pages" in survey))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const page of survey["pages"])
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user