mirror of
https://github.com/psychopy/psychojs.git
synced 2025-05-10 10:40:54 +00:00
added MultiStairHandler
This commit is contained in:
parent
dd0c46ccd0
commit
9c3c27201d
@ -17,7 +17,7 @@ module.exports = {
|
|||||||
"block-spacing": 2,
|
"block-spacing": 2,
|
||||||
"brace-style": [2, "allman", { allowSingleLine: true }],
|
"brace-style": [2, "allman", { allowSingleLine: true }],
|
||||||
"camelcase": 1,
|
"camelcase": 1,
|
||||||
"capitalized-comments": [1, "always", { ignoreConsecutiveComments: true }],
|
"capitalized-comments": 0,
|
||||||
"comma-spacing": 2,
|
"comma-spacing": 2,
|
||||||
"comma-style": 2,
|
"comma-style": 2,
|
||||||
"consistent-return": 1,
|
"consistent-return": 1,
|
||||||
@ -47,7 +47,7 @@ module.exports = {
|
|||||||
"no-console": 1,
|
"no-console": 1,
|
||||||
"no-div-regex": 2,
|
"no-div-regex": 2,
|
||||||
"no-duplicate-imports": 2,
|
"no-duplicate-imports": 2,
|
||||||
"no-else-return": 2,
|
"no-else-return": 1,
|
||||||
"no-eval": 2,
|
"no-eval": 2,
|
||||||
"no-extend-native": 2,
|
"no-extend-native": 2,
|
||||||
"no-extra-bind": 2,
|
"no-extra-bind": 2,
|
||||||
@ -65,7 +65,7 @@ module.exports = {
|
|||||||
"no-mixed-requires": 2,
|
"no-mixed-requires": 2,
|
||||||
"no-multi-spaces": 2,
|
"no-multi-spaces": 2,
|
||||||
"no-multi-str": 2,
|
"no-multi-str": 2,
|
||||||
"no-multiple-empty-lines": [2, { max: 1, maxEOF: 0 }],
|
"no-multiple-empty-lines": [1, { max: 2, maxEOF: 0 }],
|
||||||
"no-new": 2,
|
"no-new": 2,
|
||||||
"no-new-func": 2,
|
"no-new-func": 2,
|
||||||
"no-new-object": 2,
|
"no-new-object": 2,
|
||||||
@ -74,7 +74,7 @@ module.exports = {
|
|||||||
"no-octal-escape": 2,
|
"no-octal-escape": 2,
|
||||||
"no-param-reassign": 1,
|
"no-param-reassign": 1,
|
||||||
"no-path-concat": 2,
|
"no-path-concat": 2,
|
||||||
"no-plusplus": 2,
|
"no-plusplus": 0,
|
||||||
"no-proto": 2,
|
"no-proto": 2,
|
||||||
"no-restricted-properties": 2,
|
"no-restricted-properties": 2,
|
||||||
"no-return-assign": [2, "except-parens"],
|
"no-return-assign": [2, "except-parens"],
|
||||||
@ -85,14 +85,15 @@ module.exports = {
|
|||||||
"no-shadow-restricted-names": 2,
|
"no-shadow-restricted-names": 2,
|
||||||
"no-tabs": [1, { allowIndentationTabs: true }],
|
"no-tabs": [1, { allowIndentationTabs: true }],
|
||||||
"no-template-curly-in-string": 2,
|
"no-template-curly-in-string": 2,
|
||||||
"no-throw-literal": 2,
|
"no-throw-literal": 0,
|
||||||
"no-trailing-spaces": 2,
|
"no-trailing-spaces": 2,
|
||||||
"no-undef-init": 2,
|
"no-undef-init": 2,
|
||||||
// https://eslint.org/docs/rules/no-underscore-dangle#disallow-dangling-underscores-in-identifiers-no-underscore-dangle
|
// https://eslint.org/docs/rules/no-underscore-dangle#disallow-dangling-underscores-in-identifiers-no-underscore-dangle
|
||||||
"no-underscore-dangle": 1,
|
"no-underscore-dangle": 0,
|
||||||
"no-unmodified-loop-condition": 2,
|
"no-unmodified-loop-condition": 2,
|
||||||
"no-unneeded-ternary": 2,
|
"no-unneeded-ternary": 2,
|
||||||
"no-unused-expressions": 2,
|
"no-unused-expressions": 1,
|
||||||
|
"no-unused-vars": 1,
|
||||||
"no-use-before-define": [2, { functions: false }],
|
"no-use-before-define": [2, { functions: false }],
|
||||||
"no-useless-call": 2,
|
"no-useless-call": 2,
|
||||||
"no-useless-computed-key": 2,
|
"no-useless-computed-key": 2,
|
||||||
@ -106,7 +107,7 @@ module.exports = {
|
|||||||
"object-property-newline": [2, { allowMultiplePropertiesPerLine: true }],
|
"object-property-newline": [2, { allowMultiplePropertiesPerLine: true }],
|
||||||
"one-var": [2, "never"],
|
"one-var": [2, "never"],
|
||||||
"one-var-declaration-per-line": 2,
|
"one-var-declaration-per-line": 2,
|
||||||
"operator-linebreak": [2, "before"],
|
"operator-linebreak": [1, "before"],
|
||||||
"padded-blocks": [2, "never"],
|
"padded-blocks": [2, "never"],
|
||||||
"padding-line-between-statements": 2,
|
"padding-line-between-statements": 2,
|
||||||
"prefer-const": 2,
|
"prefer-const": 2,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "psychojs",
|
"name": "psychojs",
|
||||||
"version": "2021.2.x",
|
"version": "2021.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments",
|
"description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -453,7 +453,7 @@ export class ServerManager extends PsychObject
|
|||||||
path,
|
path,
|
||||||
data: undefined,
|
data: undefined,
|
||||||
});
|
});
|
||||||
this._psychoJS.logger.debug("registered resource:", name, path);
|
this._psychoJS.logger.debug(`registered resource: name= ${name}, path= ${path}`);
|
||||||
resourcesToDownload.add(name);
|
resourcesToDownload.add(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -476,12 +476,10 @@ export class ServerManager extends PsychObject
|
|||||||
{
|
{
|
||||||
// to deal with potential CORS issues, we use the pavlovia.org proxy for resources
|
// to deal with potential CORS issues, we use the pavlovia.org proxy for resources
|
||||||
// not hosted on pavlovia.org:
|
// not hosted on pavlovia.org:
|
||||||
if (
|
if ( (path.toLowerCase().indexOf("www.") === 0 ||
|
||||||
(path.toLowerCase().indexOf("www.") === 0
|
path.toLowerCase().indexOf("http:") === 0 ||
|
||||||
|| path.toLowerCase().indexOf("http:") === 0
|
path.toLowerCase().indexOf("https:") === 0) &&
|
||||||
|| path.toLowerCase().indexOf("https:") === 0)
|
(path.indexOf("pavlovia.org") === -1) )
|
||||||
&& (path.indexOf("pavlovia.org") === -1)
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
path = "https://pavlovia.org/api/v2/proxy/" + path;
|
path = "https://pavlovia.org/api/v2/proxy/" + path;
|
||||||
}
|
}
|
||||||
@ -491,7 +489,7 @@ export class ServerManager extends PsychObject
|
|||||||
path,
|
path,
|
||||||
data: undefined,
|
data: undefined,
|
||||||
});
|
});
|
||||||
this._psychoJS.logger.debug("registered resource:", name, path);
|
this._psychoJS.logger.debug(`registered resource: name= ${name}, path= ${path}`);
|
||||||
|
|
||||||
// download resources by default:
|
// download resources by default:
|
||||||
if (typeof download === "undefined" || download)
|
if (typeof download === "undefined" || download)
|
||||||
|
382
src/data/MultiStairHandler.js
Normal file
382
src/data/MultiStairHandler.js
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
/** @module data */
|
||||||
|
/**
|
||||||
|
* Multiple Staircase Trial Handler
|
||||||
|
*
|
||||||
|
* @author Alain Pitiot
|
||||||
|
* @version 2021.2.1
|
||||||
|
* @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2021 Open Science Tools Ltd.
|
||||||
|
* (https://opensciencetools.org)
|
||||||
|
* @license Distributed under the terms of the MIT License
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import {TrialHandler} from "./TrialHandler.js";
|
||||||
|
import {QuestHandler} from "./QuestHandler.js";
|
||||||
|
import * as util from "../util/Util.js";
|
||||||
|
import seedrandom from "seedrandom";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>A handler dealing with multiple staircases, simultaneously.</p>
|
||||||
|
*
|
||||||
|
* <p>Note that, at the moment, using the MultiStairHandler requires the jsQuest.js
|
||||||
|
* library to be loaded as a resource, at the start of the experiment.</p>
|
||||||
|
*
|
||||||
|
* @class module.data.MultiStairHandler
|
||||||
|
* @extends TrialHandler
|
||||||
|
* @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 the staircases
|
||||||
|
* @param {module:data.MultiStairHandler.StaircaseType} [options.stairType="simple"] - the
|
||||||
|
* handler type
|
||||||
|
* @param {Array.<Object> | String} [options.conditions= [undefined] ] - if it is a string,
|
||||||
|
* we treat it as the name of a conditions resource
|
||||||
|
* @param {module:data.TrialHandler.Method} options.method - the trial method
|
||||||
|
* @param {number} [options.nTrials=50] - maximum number of trials
|
||||||
|
* @param {number} options.randomSeed - seed for the random number generator
|
||||||
|
* @param {string} options.name - name of the handler
|
||||||
|
* @param {boolean} [options.autoLog= false] - whether or not to log
|
||||||
|
*/
|
||||||
|
export class MultiStairHandler extends TrialHandler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
constructor({
|
||||||
|
psychoJS,
|
||||||
|
varName,
|
||||||
|
stairType,
|
||||||
|
conditions,
|
||||||
|
method = TrialHandler.Method.RANDOM,
|
||||||
|
nTrials = 50,
|
||||||
|
randomSeed,
|
||||||
|
name,
|
||||||
|
autoLog
|
||||||
|
} = {})
|
||||||
|
{
|
||||||
|
super({
|
||||||
|
psychoJS,
|
||||||
|
name,
|
||||||
|
autoLog,
|
||||||
|
seed: randomSeed,
|
||||||
|
// note: multiStairHandler is a sequential TrialHandler, we deal with randomness
|
||||||
|
// in _nextTrial
|
||||||
|
method: TrialHandler.Method.SEQUENTIAL,
|
||||||
|
trialList: Array(nTrials),
|
||||||
|
nReps: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// now that we have initialised a sequential TrialHandler, we update method:
|
||||||
|
this._multiMethod = method;
|
||||||
|
this._addAttribute("varName", varName);
|
||||||
|
this._addAttribute("stairType", stairType, MultiStairHandler.StaircaseType.SIMPLE);
|
||||||
|
this._addAttribute("conditions", conditions, [undefined]);
|
||||||
|
this._addAttribute("nTrials", nTrials);
|
||||||
|
|
||||||
|
if (typeof randomSeed !== "undefined")
|
||||||
|
{
|
||||||
|
this._randomNumberGenerator = seedrandom(randomSeed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this._randomNumberGenerator = seedrandom();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._prepareStaircases();
|
||||||
|
this._nextTrial();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a response to the current staircase.
|
||||||
|
*
|
||||||
|
* @name module:data.MultiStairHandler#addResponse
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
* @param{number} response - the response to the trial, must be either 0 (incorrect or
|
||||||
|
* non-detected) or 1 (correct or detected).
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
addResponse(response)
|
||||||
|
{
|
||||||
|
// check that response is either 0 or 1:
|
||||||
|
if (response !== 0 && response !== 1)
|
||||||
|
{
|
||||||
|
throw {
|
||||||
|
origin: "MultiStairHandler.addResponse",
|
||||||
|
context: "when adding a trial response",
|
||||||
|
error: `the response must be either 0 or 1, got: ${JSON.stringify(response)}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._finished)
|
||||||
|
{
|
||||||
|
// update the current staircase:
|
||||||
|
this._currentStaircase.addResponse(response);
|
||||||
|
|
||||||
|
// move onto the next trial:
|
||||||
|
this._nextTrial();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the conditions.
|
||||||
|
*
|
||||||
|
* @name module:data.MultiStairHandler#_validateConditions
|
||||||
|
* @function
|
||||||
|
* @protected
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_validateConditions()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// conditions must be a non empty array:
|
||||||
|
if (!Array.isArray(this._conditions) || this._conditions.length === 0)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
// each condition must be an object:
|
||||||
|
if (typeof condition !== "object")
|
||||||
|
{
|
||||||
|
throw "one of the conditions is not an object";
|
||||||
|
}
|
||||||
|
|
||||||
|
// each condition must include certain fields, such as startVal and label:
|
||||||
|
if (!("startVal" in condition))
|
||||||
|
{
|
||||||
|
throw "each condition should include a startVal field";
|
||||||
|
}
|
||||||
|
if (!("label" in condition))
|
||||||
|
{
|
||||||
|
throw "each condition should include a label field";
|
||||||
|
}
|
||||||
|
|
||||||
|
// for QUEST, we also need startValSd:
|
||||||
|
if (this._stairType === MultiStairHandler.StaircaseType.QUEST && !("startValSd" in condition))
|
||||||
|
{
|
||||||
|
throw "QUEST conditions must include a startValSd field";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
throw {
|
||||||
|
origin: "MultiStairHandler._validateConditions",
|
||||||
|
context: "when validating the conditions",
|
||||||
|
error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the staircases, according to the conditions.
|
||||||
|
*
|
||||||
|
* @name module:data.MultiStairHandler#_prepareStaircases
|
||||||
|
* @function
|
||||||
|
* @protected
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_prepareStaircases()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this._validateConditions();
|
||||||
|
|
||||||
|
this._staircases = [];
|
||||||
|
|
||||||
|
for (const condition of this._conditions)
|
||||||
|
{
|
||||||
|
let handler;
|
||||||
|
|
||||||
|
// QUEST handler:
|
||||||
|
if (this._stairType === MultiStairHandler.StaircaseType.QUEST)
|
||||||
|
{
|
||||||
|
const args = Object.assign({}, condition);
|
||||||
|
args.psychoJS = this._psychoJS;
|
||||||
|
args.varName = this._varName;
|
||||||
|
args.name = condition.label;
|
||||||
|
args.autoLog = this._autoLog;
|
||||||
|
if (typeof condition.nTrials === "undefined")
|
||||||
|
{
|
||||||
|
args.nTrials = this._nTrials;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler = new QuestHandler(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// simple StairCase handler:
|
||||||
|
if (this._stairType === MultiStairHandler.StaircaseType.SIMPLE)
|
||||||
|
{
|
||||||
|
// TODO not supported just yet, an exception is raised in _validateConditions
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._staircases.push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._currentPass = [];
|
||||||
|
this._currentStaircase = null;
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
throw {
|
||||||
|
origin: "MultiStairHandler._prepareStaircases",
|
||||||
|
context: "when preparing the staircases",
|
||||||
|
error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move onto the next trial.
|
||||||
|
*
|
||||||
|
* @name module:data.MultiStairHandler#_nextTrial
|
||||||
|
* @function
|
||||||
|
* @protected
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_nextTrial()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// if the current pass is empty, get a new one:
|
||||||
|
if (this._currentPass.length === 0)
|
||||||
|
{
|
||||||
|
this._currentPass = this._staircases.filter(handler => !handler.finished);
|
||||||
|
|
||||||
|
if (this._multiMethod === TrialHandler.Method.SEQUENTIAL)
|
||||||
|
{
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
else if (this._multiMethod === TrialHandler.Method.RANDOM)
|
||||||
|
{
|
||||||
|
this._currentPass = util.shuffle(this._currentPass, this._randomNumberGenerator);
|
||||||
|
}
|
||||||
|
else if (this._multiMethod === TrialHandler.Method.FULL_RANDOM)
|
||||||
|
{
|
||||||
|
if (this._currentPass.length > 0)
|
||||||
|
{
|
||||||
|
// select a handler at random:
|
||||||
|
const index = Math.floor(this._randomNumberGenerator() * this._currentPass.length);
|
||||||
|
const handler = this._currentPass[index];
|
||||||
|
this._currentPass = [handler];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// pick the next staircase in the pass:
|
||||||
|
this._currentStaircase = this._currentPass.shift();
|
||||||
|
|
||||||
|
|
||||||
|
// test for termination:
|
||||||
|
if (typeof this._currentStaircase === "undefined")
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// get the value, based on the type of the trial handler:
|
||||||
|
let value = Number.MIN_VALUE;
|
||||||
|
if (this._currentStaircase instanceof QuestHandler)
|
||||||
|
{
|
||||||
|
value = this._currentStaircase.getQuestValue();
|
||||||
|
}
|
||||||
|
// TODO add a test for simple staircase:
|
||||||
|
// if (this._currentStaircase instanceof StaircaseHandler)
|
||||||
|
// {
|
||||||
|
// value = this._currentStaircase.getStairValue();
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
this._psychoJS.logger.debug(`selected staircase: ${this._currentStaircase.name}, estimated value for variable ${this._varName}: ${value}`);
|
||||||
|
|
||||||
|
|
||||||
|
// update the next undefined trial in the trial list, and the associated snapshot:
|
||||||
|
for (let t = 0; t < this._trialList.length; ++t)
|
||||||
|
{
|
||||||
|
if (typeof this._trialList[t] === "undefined")
|
||||||
|
{
|
||||||
|
this._trialList[t] = {[this._varName]: value};
|
||||||
|
|
||||||
|
if (typeof this._snapshots[t] !== "undefined")
|
||||||
|
{
|
||||||
|
this._snapshots[t][this._varName] = value;
|
||||||
|
this._snapshots[t].trialAttributes.push(this._varName);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
throw {
|
||||||
|
origin: "MultiStairHandler._nextTrial",
|
||||||
|
context: "when moving onto the next trial",
|
||||||
|
error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MultiStairHandler staircase type.
|
||||||
|
*
|
||||||
|
* @enum {Symbol}
|
||||||
|
* @readonly
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
MultiStairHandler.StaircaseType = {
|
||||||
|
/**
|
||||||
|
* Simple staircase handler.
|
||||||
|
*/
|
||||||
|
SIMPLE: Symbol.for("SIMPLE"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* QUEST handler.
|
||||||
|
*/
|
||||||
|
QUEST: Symbol.for("QUEST")
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Staircase status.
|
||||||
|
*
|
||||||
|
* @enum {Symbol}
|
||||||
|
* @readonly
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
MultiStairHandler.StaircaseStatus = {
|
||||||
|
/**
|
||||||
|
* The staircase is currently running.
|
||||||
|
*/
|
||||||
|
RUNNING: Symbol.for("RUNNING"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The staircase is now finished.
|
||||||
|
*/
|
||||||
|
FINISHED: Symbol.for("FINISHED")
|
||||||
|
};
|
@ -9,7 +9,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import {TrialHandler} from "./TrialHandler.js";
|
import {TrialHandler} from "./TrialHandler.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -18,7 +17,7 @@ import {TrialHandler} from "./TrialHandler.js";
|
|||||||
*
|
*
|
||||||
* @class module.data.QuestHandler
|
* @class module.data.QuestHandler
|
||||||
* @extends TrialHandler
|
* @extends TrialHandler
|
||||||
* @param {Object} options
|
* @param {Object} options - the handler options
|
||||||
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
* @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 {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.startVal - initial guess for the threshold
|
||||||
@ -70,25 +69,25 @@ export class QuestHandler extends TrialHandler
|
|||||||
nReps: 1
|
nReps: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
this._addAttribute('varName', varName);
|
this._addAttribute("varName", varName);
|
||||||
this._addAttribute('startVal', startVal);
|
this._addAttribute("startVal", startVal);
|
||||||
this._addAttribute('minVal', minVal, Number.MIN_VALUE);
|
this._addAttribute("minVal", minVal, Number.MIN_VALUE);
|
||||||
this._addAttribute('maxVal', maxVal, Number.MAX_VALUE);
|
this._addAttribute("maxVal", maxVal, Number.MAX_VALUE);
|
||||||
this._addAttribute('startValSd', startValSd);
|
this._addAttribute("startValSd", startValSd);
|
||||||
this._addAttribute('pThreshold', pThreshold, 0.82);
|
this._addAttribute("pThreshold", pThreshold, 0.82);
|
||||||
this._addAttribute('nTrials', nTrials);
|
this._addAttribute("nTrials", nTrials);
|
||||||
this._addAttribute('stopInterval', stopInterval, Number.MIN_VALUE);
|
this._addAttribute("stopInterval", stopInterval, Number.MIN_VALUE);
|
||||||
this._addAttribute('beta', beta, 3.5);
|
this._addAttribute("beta", beta, 3.5);
|
||||||
this._addAttribute('delta', delta, 0.01);
|
this._addAttribute("delta", delta, 0.01);
|
||||||
this._addAttribute('gamma', gamma, 0.5);
|
this._addAttribute("gamma", gamma, 0.5);
|
||||||
this._addAttribute('grain', grain, 0.01);
|
this._addAttribute("grain", grain, 0.01);
|
||||||
this._addAttribute('method', method, QuestHandler.Method.QUANTILE);
|
this._addAttribute("method", method, QuestHandler.Method.QUANTILE);
|
||||||
|
|
||||||
// setup jsQuest:
|
// setup jsQuest:
|
||||||
this._setupJsQuest();
|
this._setupJsQuest();
|
||||||
|
this._estimateQuestValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a response and update the PDF.
|
* Add a response and update the PDF.
|
||||||
*
|
*
|
||||||
@ -97,6 +96,7 @@ export class QuestHandler extends TrialHandler
|
|||||||
* @public
|
* @public
|
||||||
* @param{number} response - the response to the trial, must be either 0 (incorrect or
|
* @param{number} response - the response to the trial, must be either 0 (incorrect or
|
||||||
* non-detected) or 1 (correct or detected).
|
* non-detected) or 1 (correct or detected).
|
||||||
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
addResponse(response)
|
addResponse(response)
|
||||||
{
|
{
|
||||||
@ -104,8 +104,8 @@ export class QuestHandler extends TrialHandler
|
|||||||
if (response !== 0 && response !== 1)
|
if (response !== 0 && response !== 1)
|
||||||
{
|
{
|
||||||
throw {
|
throw {
|
||||||
origin: 'QuestHandler.addResponse',
|
origin: "QuestHandler.addResponse",
|
||||||
context: 'when adding a trial response',
|
context: "when adding a trial response",
|
||||||
error: `the response must be either 0 or 1, got: ${JSON.stringify(response)}`
|
error: `the response must be either 0 or 1, got: ${JSON.stringify(response)}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -120,7 +120,6 @@ export class QuestHandler extends TrialHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simulate a response.
|
* Simulate a response.
|
||||||
*
|
*
|
||||||
@ -128,6 +127,7 @@ export class QuestHandler extends TrialHandler
|
|||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @param{number} trueValue - the true, known value of the threshold / contrast / intensity
|
* @param{number} trueValue - the true, known value of the threshold / contrast / intensity
|
||||||
|
* @returns{number} the simulated response, 0 or 1
|
||||||
*/
|
*/
|
||||||
simulate(trueValue)
|
simulate(trueValue)
|
||||||
{
|
{
|
||||||
@ -141,7 +141,6 @@ export class QuestHandler extends TrialHandler
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the mean of the Quest posterior PDF.
|
* Get the mean of the Quest posterior PDF.
|
||||||
*
|
*
|
||||||
@ -155,7 +154,6 @@ export class QuestHandler extends TrialHandler
|
|||||||
return jsQUEST.QuestMean(this._jsQuest);
|
return jsQUEST.QuestMean(this._jsQuest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the standard deviation of the Quest posterior PDF.
|
* Get the standard deviation of the Quest posterior PDF.
|
||||||
*
|
*
|
||||||
@ -169,7 +167,6 @@ export class QuestHandler extends TrialHandler
|
|||||||
return jsQUEST.QuestSd(this._jsQuest);
|
return jsQUEST.QuestSd(this._jsQuest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the mode of the Quest posterior PDF.
|
* Get the mode of the Quest posterior PDF.
|
||||||
*
|
*
|
||||||
@ -184,7 +181,6 @@ export class QuestHandler extends TrialHandler
|
|||||||
return mode;
|
return mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the standard deviation of the Quest posterior PDF.
|
* Get the standard deviation of the Quest posterior PDF.
|
||||||
*
|
*
|
||||||
@ -199,6 +195,18 @@ export class QuestHandler extends TrialHandler
|
|||||||
return jsQUEST.QuestQuantile(this._jsQuest, quantileOrder);
|
return jsQUEST.QuestQuantile(this._jsQuest, quantileOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current value of the variable / contrast / threshold.
|
||||||
|
*
|
||||||
|
* @name module:data.QuestHandler#getQuestValue
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
* @returns {number} the current QUEST value for the variable / contrast / threshold
|
||||||
|
*/
|
||||||
|
getQuestValue()
|
||||||
|
{
|
||||||
|
return this._questValue;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an estimate of the 5%-95% confidence interval (CI).
|
* Get an estimate of the 5%-95% confidence interval (CI).
|
||||||
@ -206,7 +214,8 @@ export class QuestHandler extends TrialHandler
|
|||||||
* @name module:data.QuestHandler#confInterval
|
* @name module:data.QuestHandler#confInterval
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
* @param{boolean} [getDifference=false] if true, return the width of the CI instead of the CI
|
* @param{boolean} [getDifference=false] - if true, return the width of the CI instead of the CI
|
||||||
|
* @returns{number[] | number} the 5%-95% CI or the width of the CI
|
||||||
*/
|
*/
|
||||||
confInterval(getDifference = false)
|
confInterval(getDifference = false)
|
||||||
{
|
{
|
||||||
@ -225,13 +234,13 @@ export class QuestHandler extends TrialHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup the JS Quest object.
|
* Setup the JS Quest object.
|
||||||
*
|
*
|
||||||
* @name module:data.QuestHandler#_setupJsQuest
|
* @name module:data.QuestHandler#_setupJsQuest
|
||||||
* @function
|
* @function
|
||||||
* @protected
|
* @protected
|
||||||
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_setupJsQuest()
|
_setupJsQuest()
|
||||||
{
|
{
|
||||||
@ -243,11 +252,8 @@ export class QuestHandler extends TrialHandler
|
|||||||
this._delta,
|
this._delta,
|
||||||
this._gamma,
|
this._gamma,
|
||||||
this._grain);
|
this._grain);
|
||||||
|
|
||||||
this._estimateQuestValue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Estimate the next value of the QUEST variable, based on the current value
|
* Estimate the next value of the QUEST variable, based on the current value
|
||||||
* and on the selected QUEST method.
|
* and on the selected QUEST method.
|
||||||
@ -255,6 +261,7 @@ export class QuestHandler extends TrialHandler
|
|||||||
* @name module:data.QuestHandler#_estimateQuestValue
|
* @name module:data.QuestHandler#_estimateQuestValue
|
||||||
* @function
|
* @function
|
||||||
* @protected
|
* @protected
|
||||||
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_estimateQuestValue()
|
_estimateQuestValue()
|
||||||
{
|
{
|
||||||
@ -275,8 +282,8 @@ export class QuestHandler extends TrialHandler
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw {
|
throw {
|
||||||
origin: 'QuestHandler._estimateQuestValue',
|
origin: "QuestHandler._estimateQuestValue",
|
||||||
context: 'when estimating the next value of the QUEST variable',
|
context: "when estimating the next value of the QUEST variable",
|
||||||
error: `unknown method: ${this._method}, please use: mean, mode, or quantile`
|
error: `unknown method: ${this._method}, please use: mean, mode, or quantile`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -284,16 +291,15 @@ export class QuestHandler extends TrialHandler
|
|||||||
this._psychoJS.logger.debug(`estimated value for QUEST variable ${this._varName}: ${this._questValue}`);
|
this._psychoJS.logger.debug(`estimated value for QUEST variable ${this._varName}: ${this._questValue}`);
|
||||||
|
|
||||||
// check whether we should finish the trial:
|
// check whether we should finish the trial:
|
||||||
if (this.thisN > 0 &&
|
if (this.thisN > 0 && (this.nRemaining === 0 || this.confInterval(true) < this._stopInterval))
|
||||||
(this.nRemaining === 0 || this.confInterval(true) < this._stopInterval))
|
|
||||||
{
|
{
|
||||||
this._finished = true;
|
this._finished = true;
|
||||||
|
|
||||||
// update the snapshots associated with the current trial in the trial list:
|
// update the snapshots associated with the current trial in the trial list:
|
||||||
for (let t = 0; t < this._trialList.length-1; ++t)
|
for (let t = 0; t < this._snapshots.length - 1; ++t)
|
||||||
{
|
{
|
||||||
// the current trial is the last defined one:
|
// the current trial is the last defined one:
|
||||||
if (typeof this._trialList[t+1] === 'undefined')
|
if (typeof this._trialList[t + 1] === "undefined")
|
||||||
{
|
{
|
||||||
this._snapshots[t].finished = true;
|
this._snapshots[t].finished = true;
|
||||||
break;
|
break;
|
||||||
@ -306,11 +312,11 @@ export class QuestHandler extends TrialHandler
|
|||||||
// update the next undefined trial in the trial list, and the associated snapshot:
|
// update the next undefined trial in the trial list, and the associated snapshot:
|
||||||
for (let t = 0; t < this._trialList.length; ++t)
|
for (let t = 0; t < this._trialList.length; ++t)
|
||||||
{
|
{
|
||||||
if (typeof this._trialList[t] === 'undefined')
|
if (typeof this._trialList[t] === "undefined")
|
||||||
{
|
{
|
||||||
this._trialList[t] = { [this._varName]: this._questValue };
|
this._trialList[t] = { [this._varName]: this._questValue };
|
||||||
|
|
||||||
if (typeof this._snapshots[t] !== 'undefined')
|
if (typeof this._snapshots[t] !== "undefined")
|
||||||
{
|
{
|
||||||
this._snapshots[t][this._varName] = this._questValue;
|
this._snapshots[t][this._varName] = this._questValue;
|
||||||
this._snapshots[t].trialAttributes.push(this._varName);
|
this._snapshots[t].trialAttributes.push(this._varName);
|
||||||
@ -319,10 +325,8 @@ export class QuestHandler extends TrialHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* QuestHandler method
|
* QuestHandler method
|
||||||
*
|
*
|
||||||
@ -334,15 +338,15 @@ QuestHandler.Method = {
|
|||||||
/**
|
/**
|
||||||
* Quantile threshold estimate.
|
* Quantile threshold estimate.
|
||||||
*/
|
*/
|
||||||
QUANTILE: Symbol.for('QUANTILE'),
|
QUANTILE: Symbol.for("QUANTILE"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mean threshold estimate.
|
* Mean threshold estimate.
|
||||||
*/
|
*/
|
||||||
MEAN: Symbol.for('MEAN'),
|
MEAN: Symbol.for("MEAN"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mode threshold estimate.
|
* Mode threshold estimate.
|
||||||
*/
|
*/
|
||||||
MODE: Symbol.for('MODE')
|
MODE: Symbol.for("MODE")
|
||||||
};
|
};
|
||||||
|
@ -19,7 +19,7 @@ import * as util from "../util/Util.js";
|
|||||||
*
|
*
|
||||||
* @class
|
* @class
|
||||||
* @extends PsychObject
|
* @extends PsychObject
|
||||||
* @param {Object} options
|
* @param {Object} options - the handler options
|
||||||
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
|
||||||
* @param {Array.<Object> | String} [options.trialList= [undefined] ] - if it is a string, we treat it as the name of a condition resource
|
* @param {Array.<Object> | String} [options.trialList= [undefined] ] - if it is a string, we treat it as the name of a condition resource
|
||||||
* @param {number} options.nReps - number of repetitions
|
* @param {number} options.nReps - number of repetitions
|
||||||
@ -80,7 +80,7 @@ export class TrialHandler extends PsychObject
|
|||||||
this._addAttribute("name", name);
|
this._addAttribute("name", name);
|
||||||
this._addAttribute("autoLog", autoLog);
|
this._addAttribute("autoLog", autoLog);
|
||||||
this._addAttribute("seed", seed);
|
this._addAttribute("seed", seed);
|
||||||
this._prepareTrialList(trialList);
|
this._prepareTrialList();
|
||||||
|
|
||||||
// number of stimuli
|
// number of stimuli
|
||||||
this.nStim = this.trialList.length;
|
this.nStim = this.trialList.length;
|
||||||
@ -520,7 +520,7 @@ export class TrialHandler extends PsychObject
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
let resourceExtension = resourceName.split(".").pop();
|
const resourceExtension = resourceName.split(".").pop();
|
||||||
if (["csv", "odp", "xls", "xlsx"].indexOf(resourceExtension) > -1)
|
if (["csv", "odp", "xls", "xlsx"].indexOf(resourceExtension) > -1)
|
||||||
{
|
{
|
||||||
// (*) read conditions from resource:
|
// (*) read conditions from resource:
|
||||||
@ -617,10 +617,11 @@ export class TrialHandler extends PsychObject
|
|||||||
/**
|
/**
|
||||||
* Prepare the trial list.
|
* Prepare the trial list.
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @protected
|
* @protected
|
||||||
* @param {Array.<Object> | String} trialList - a list of trials, or the name of a condition resource
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_prepareTrialList(trialList)
|
_prepareTrialList()
|
||||||
{
|
{
|
||||||
const response = {
|
const response = {
|
||||||
origin: "TrialHandler._prepareTrialList",
|
origin: "TrialHandler._prepareTrialList",
|
||||||
@ -628,28 +629,28 @@ export class TrialHandler extends PsychObject
|
|||||||
};
|
};
|
||||||
|
|
||||||
// we treat undefined trialList as a list with a single empty entry:
|
// we treat undefined trialList as a list with a single empty entry:
|
||||||
if (typeof trialList === "undefined")
|
if (typeof this._trialList === "undefined")
|
||||||
{
|
{
|
||||||
this.trialList = [undefined];
|
this.trialList = [undefined];
|
||||||
}
|
}
|
||||||
// if trialList is an array, we make sure it is not empty:
|
// if trialList is an array, we make sure it is not empty:
|
||||||
else if (Array.isArray(trialList))
|
else if (Array.isArray(this._trialList))
|
||||||
{
|
{
|
||||||
if (trialList.length === 0)
|
if (this._trialList.length === 0)
|
||||||
{
|
{
|
||||||
this.trialList = [undefined];
|
this.trialList = [undefined];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if trialList is a string, we treat it as the name of the condition resource:
|
// if trialList is a string, we treat it as the name of the condition resource:
|
||||||
else if (typeof trialList === "string")
|
else if (typeof this._trialList === "string")
|
||||||
{
|
{
|
||||||
this.trialList = TrialHandler.importConditions(this.psychoJS.serverManager, trialList);
|
this.trialList = TrialHandler.importConditions(this.psychoJS.serverManager, this._trialList);
|
||||||
}
|
}
|
||||||
// unknown type:
|
// unknown type:
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw Object.assign(response, {
|
throw Object.assign(response, {
|
||||||
error: "unable to prepare trial list: unknown type: " + (typeof trialList),
|
error: `unable to prepare trial list: unknown type: ${(typeof this._trialList)}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -690,13 +691,13 @@ export class TrialHandler extends PsychObject
|
|||||||
// get an array of the indices of the elements of trialList :
|
// get an array of the indices of the elements of trialList :
|
||||||
const indices = Array.from(this.trialList.keys());
|
const indices = Array.from(this.trialList.keys());
|
||||||
|
|
||||||
if (this.method === TrialHandler.Method.SEQUENTIAL)
|
if (this._method === TrialHandler.Method.SEQUENTIAL)
|
||||||
{
|
{
|
||||||
this._trialSequence = Array(this.nReps).fill(indices);
|
this._trialSequence = Array(this.nReps).fill(indices);
|
||||||
// transposed version:
|
// transposed version:
|
||||||
// this._trialSequence = indices.reduce( (seq, e) => { seq.push( Array(this.nReps).fill(e) ); return seq; }, [] );
|
// this._trialSequence = indices.reduce( (seq, e) => { seq.push( Array(this.nReps).fill(e) ); return seq; }, [] );
|
||||||
}
|
}
|
||||||
else if (this.method === TrialHandler.Method.RANDOM)
|
else if (this._method === TrialHandler.Method.RANDOM)
|
||||||
{
|
{
|
||||||
this._trialSequence = [];
|
this._trialSequence = [];
|
||||||
for (let i = 0; i < this.nReps; ++i)
|
for (let i = 0; i < this.nReps; ++i)
|
||||||
@ -704,10 +705,10 @@ export class TrialHandler extends PsychObject
|
|||||||
this._trialSequence.push(util.shuffle(indices.slice(), this._randomNumberGenerator));
|
this._trialSequence.push(util.shuffle(indices.slice(), this._randomNumberGenerator));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (this.method === TrialHandler.Method.FULL_RANDOM)
|
else if (this._method === TrialHandler.Method.FULL_RANDOM)
|
||||||
{
|
{
|
||||||
// create a flat sequence with nReps repeats of indices:
|
// create a flat sequence with nReps repeats of indices:
|
||||||
let flatSequence = [];
|
const flatSequence = [];
|
||||||
for (let i = 0; i < this.nReps; ++i)
|
for (let i = 0; i < this.nReps; ++i)
|
||||||
{
|
{
|
||||||
flatSequence.push.apply(flatSequence, indices);
|
flatSequence.push.apply(flatSequence, indices);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export * from './ExperimentHandler.js';
|
export * from "./ExperimentHandler.js";
|
||||||
export * from './TrialHandler.js';
|
export * from "./TrialHandler.js";
|
||||||
export * from './QuestHandler';
|
export * from "./QuestHandler.js";
|
||||||
//export * from './Shelf.js';
|
export * from "./MultiStairHandler.js";
|
||||||
|
//export * from "./Shelf.js";
|
||||||
|
Loading…
Reference in New Issue
Block a user