1
0
mirror of https://github.com/psychopy/psychojs.git synced 2025-05-10 10:40:54 +00:00
psychojs/docs/data_ExperimentHandler.js.html
2021-06-02 13:53:50 +02:00

505 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: data/ExperimentHandler.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: data/ExperimentHandler.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* Experiment Handler
*
* @author Alain Pitiot
* @version 2021.2.0
* @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 * as XLSX from 'xlsx';
import {PsychObject} from '../util/PsychObject';
import {MonotonicClock} from '../util/Clock';
import * as util from '../util/Util';
/**
* &lt;p>An ExperimentHandler keeps track of multiple loops and handlers. It is particularly useful
* for generating a single data file from an experiment with many different loops (e.g. interleaved
* staircases or loops within loops.&lt;/p>
*
* @name module:data.ExperimentHandler
* @class
* @extends PsychObject
* @param {Object} options
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
* @param {string} options.name - name of the experiment
* @param {Object} options.extraInfo - additional information, such as session name, participant name, etc.
*/
export class ExperimentHandler extends PsychObject
{
/**
* Getter for experimentEnded.
*
* @name module:core.Window#experimentEnded
* @function
* @public
*/
get experimentEnded()
{
return this._experimentEnded;
}
/**
* Setter for experimentEnded.
*
* @name module:core.Window#experimentEnded
* @function
* @public
*/
set experimentEnded(ended)
{
this._experimentEnded = ended;
}
/**
* Legacy experiment getters.
*/
get _thisEntry()
{
return this._currentTrialData;
}
get _entries()
{
return this._trialsData;
}
constructor({
psychoJS,
name,
extraInfo
} = {})
{
super(psychoJS, name);
this._addAttribute('extraInfo', extraInfo);
// loop handlers:
this._loops = [];
this._unfinishedLoops = [];
// data dictionaries (one per trial) and current data dictionary:
this._trialsKeys = [];
this._trialsData = [];
this._currentTrialData = {};
this._experimentEnded = false;
}
/**
* Whether or not the current entry (i.e. trial data) is empty.
* &lt;p>Note: this is mostly useful at the end of an experiment, in order to ensure that the last entry is saved.&lt;/p>
*
* @name module:data.ExperimentHandler#isEntryEmpty
* @function
* @public
* @returns {boolean} whether or not the current entry is empty
*/
isEntryEmpty()
{
return (Object.keys(this._currentTrialData).length > 0);
}
/**
* Add a loop.
*
* &lt;p> The loop might be a {@link TrialHandler}, for instance.&lt;/p>
* &lt;p> Data from this loop will be included in the resulting data files.&lt;/p>
*
* @name module:data.ExperimentHandler#addLoop
* @function
* @public
* @param {Object} loop - the loop, e.g. an instance of TrialHandler or StairHandler
*/
addLoop(loop)
{
this._loops.push(loop);
this._unfinishedLoops.push(loop);
loop.experimentHandler = this;
}
/**
* Remove the given loop from the list of unfinished loops, e.g. when it has completed.
*
* @name module:data.ExperimentHandler#removeLoop
* @function
* @public
* @param {Object} loop - the loop, e.g. an instance of TrialHandler or StairHandler
*/
removeLoop(loop)
{
const index = this._unfinishedLoops.indexOf(loop);
if (index !== -1)
{
this._unfinishedLoops.splice(index, 1);
}
}
/**
* Add the key/value pair.
*
* &lt;p> Multiple key/value pairs can be added to any given entry of the data file. There are
* considered part of the same entry until a call to {@link nextEntry} is made. &lt;/p>
*
* @name module:data.ExperimentHandler#addData
* @function
* @public
* @param {Object} key - the key
* @param {Object} value - the value
*/
addData(key, value)
{
if (this._trialsKeys.indexOf(key) === -1)
{
this._trialsKeys.push(key);
}
// turn arrays into their json equivalent:
if (Array.isArray(value))
{
value = JSON.stringify(value);
}
this._currentTrialData[key] = value;
}
/**
* Inform this ExperimentHandler that the current trial has ended. Further calls to {@link addData}
* will be associated with the next trial.
*
* @name module:data.ExperimentHandler#nextEntry
* @function
* @public
* @param {Object[]} snapshots - array of loop snapshots
*/
nextEntry(snapshots)
{
if (typeof snapshots !== 'undefined')
{
// turn single snapshot into a one-element array:
if (!Array.isArray(snapshots))
{
snapshots = [snapshots];
}
for (const snapshot of snapshots)
{
const attributes = ExperimentHandler._getLoopAttributes(snapshot);
for (let a in attributes)
{
if (attributes.hasOwnProperty(a))
{
this._currentTrialData[a] = attributes[a];
}
}
}
}
// this is to support legacy generated JavaScript code and does not properly handle
// loops within loops:
else
{
for (const loop of this._unfinishedLoops)
{
const attributes = ExperimentHandler._getLoopAttributes(loop);
for (const a in attributes)
{
if (attributes.hasOwnProperty(a))
{
this._currentTrialData[a] = attributes[a];
}
}
}
}
// add the extraInfo dict to the data:
for (let a in this.extraInfo)
{
if (this.extraInfo.hasOwnProperty(a))
{
this._currentTrialData[a] = this.extraInfo[a];
}
}
this._trialsData.push(this._currentTrialData);
this._currentTrialData = {};
}
/**
* Save the results of the experiment.
*
* &lt;ul>
* &lt;li>For an experiment running locally, the results are offered for immediate download.&lt;/li>
* &lt;li>For an experiment running on the server, the results are uploaded to the server.&lt;/li>
* &lt;/ul>
* &lt;p>
*
* @name module:data.ExperimentHandler#save
* @function
* @public
* @param {Object} options
* @param {Array.&lt;Object>} [options.attributes] - the attributes to be saved
* @param {Array.&lt;Object>} [options.sync] - whether or not to communicate with the server in a synchronous manner
*/
async save({
attributes = [],
sync = false
} = {})
{
this._psychoJS.logger.info('[PsychoJS] Save experiment results.');
// (*) get attributes:
if (attributes.length === 0)
{
attributes = this._trialsKeys.slice();
for (let l = 0; l &lt; this._loops.length; l++)
{
const loop = this._loops[l];
const loopAttributes = ExperimentHandler._getLoopAttributes(loop);
for (let a in loopAttributes)
{
if (loopAttributes.hasOwnProperty(a))
{
attributes.push(a);
}
}
}
for (let a in this.extraInfo)
{
if (this.extraInfo.hasOwnProperty(a))
{
attributes.push(a);
}
}
}
// (*) get various experiment info:
const info = this.extraInfo;
const __experimentName = (typeof info.expName !== 'undefined') ? info.expName : this.psychoJS.config.experiment.name;
const __participant = ((typeof info.participant === 'string' &amp;&amp; info.participant.length > 0) ? info.participant : 'PARTICIPANT');
const __session = ((typeof info.session === 'string' &amp;&amp; info.session.length > 0) ? info.session : 'SESSION');
const __datetime = ((typeof info.date !== 'undefined') ? info.date : MonotonicClock.getDateStr());
const gitlabConfig = this._psychoJS.config.gitlab;
const __projectId = (typeof gitlabConfig !== 'undefined' &amp;&amp; typeof gitlabConfig.projectId !== 'undefined') ? gitlabConfig.projectId : undefined;
// (*) save to a .csv file:
if (this._psychoJS.config.experiment.saveFormat === ExperimentHandler.SaveFormat.CSV)
{
// note: we use the XLSX library as it automatically deals with header, takes care of quotes,
// newlines, etc.
const worksheet = XLSX.utils.json_to_sheet(this._trialsData);
// prepend BOM
const csv = '\ufeff' + XLSX.utils.sheet_to_csv(worksheet);
// upload data to the pavlovia server or offer them for download:
const key = __participant + '_' + __experimentName + '_' + __datetime + '.csv';
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER &amp;&amp;
this._psychoJS.config.experiment.status === 'RUNNING' &amp;&amp;
!this._psychoJS._serverMsg.has('__pilotToken'))
{
return /*await*/ this._psychoJS.serverManager.uploadData(key, csv, sync);
}
else
{
util.offerDataForDownload(key, csv, 'text/csv');
}
}
// (*) save in the database on the remote server:
else if (this._psychoJS.config.experiment.saveFormat === ExperimentHandler.SaveFormat.DATABASE)
{
let documents = [];
for (let r = 0; r &lt; this._trialsData.length; r++)
{
let doc = {__projectId, __experimentName, __participant, __session, __datetime};
for (let h = 0; h &lt; attributes.length; h++)
{
doc[attributes[h]] = this._trialsData[r][attributes[h]];
}
documents.push(doc);
}
// upload data to the pavlovia server or offer them for download:
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER &amp;&amp;
this._psychoJS.config.experiment.status === 'RUNNING' &amp;&amp;
!this._psychoJS._serverMsg.has('__pilotToken'))
{
const key = 'results'; // name of the mongoDB collection
return /*await*/ this._psychoJS.serverManager.uploadData(key, JSON.stringify(documents), sync);
}
else
{
util.offerDataForDownload('results.json', JSON.stringify(documents), 'application/json');
}
}
}
/**
* Get the attribute names and values for the current trial of a given loop.
* &lt;p> Only info relating to the trial execution are returned.&lt;/p>
*
* @name module:data.ExperimentHandler#_getLoopAttributes
* @function
* @static
* @protected
* @param {Object} loop - the loop
*/
static _getLoopAttributes(loop)
{
// standard trial attributes:
const properties = ['thisRepN', 'thisTrialN', 'thisN', 'thisIndex', 'stepSizeCurrent', 'ran', 'order'];
let attributes = {};
const loopName = loop.name;
for (const loopProperty in loop)
{
if (properties.includes(loopProperty))
{
const key = (loopProperty === 'stepSizeCurrent') ? loopName + '.stepSize' : loopName + '.' + loopProperty;
attributes[key] = loop[loopProperty];
}
}
// specific trial attributes:
if (typeof loop.getCurrentTrial === 'function')
{
const currentTrial = loop.getCurrentTrial();
for (const trialProperty in currentTrial)
{
attributes[trialProperty] = currentTrial[trialProperty];
}
}
/* TODO
// method of constants
if hasattr(loop, 'thisTrial'):
trial = loop.thisTrial
if hasattr(trial,'items'):#is a TrialList object or a simple dict
for property,val in trial.items():
if property not in self._paramNamesSoFar:
self._paramNamesSoFar.append(property)
names.append(property)
vals.append(val)
elif trial==[]:#we haven't had 1st trial yet? Not actually sure why this occasionally happens (JWP)
pass
else:
names.append(loopName+'.thisTrial')
vals.append(trial)
// single StairHandler
elif hasattr(loop, 'intensities'):
names.append(loopName+'.intensity')
if len(loop.intensities)>0:
vals.append(loop.intensities[-1])
else:
vals.append(None)*/
return attributes;
}
}
/**
* Experiment result format
*
* @name module:core.ServerManager#SaveFormat
* @enum {Symbol}
* @readonly
* @public
*/
ExperimentHandler.SaveFormat = {
/**
* Results are saved to a .csv file
*/
CSV: Symbol.for('CSV'),
/**
* Results are saved to a database
*/
DATABASE: Symbol.for('DATABASE')
};
/**
* Experiment environment.
*
* @enum {Symbol}
* @readonly
* @public
*/
ExperimentHandler.Environment = {
SERVER: Symbol.for('SERVER'),
LOCAL: Symbol.for('LOCAL')
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-core.html">core</a></li><li><a href="module-data.html">data</a></li><li><a href="module-sound.html">sound</a></li><li><a href="module-util.html">util</a></li><li><a href="module-visual.html">visual</a></li></ul><h3>Classes</h3><ul><li><a href="module-core.BuilderKeyResponse.html">BuilderKeyResponse</a></li><li><a href="module-core.EventManager.html">EventManager</a></li><li><a href="module-core.GUI.html">GUI</a></li><li><a href="module-core.Keyboard.html">Keyboard</a></li><li><a href="module-core.KeyPress.html">KeyPress</a></li><li><a href="module-core.Logger.html">Logger</a></li><li><a href="module-core.MinimalStim.html">MinimalStim</a></li><li><a href="module-core.Mouse.html">Mouse</a></li><li><a href="module-core.PsychoJS.html">PsychoJS</a></li><li><a href="module-core.ServerManager.html">ServerManager</a></li><li><a href="module-core.Window.html">Window</a></li><li><a href="module-data.ExperimentHandler.html">ExperimentHandler</a></li><li><a href="module-data.Shelf.html">Shelf</a></li><li><a href="module-data.TrialHandler.html">TrialHandler</a></li><li><a href="module-sound.AudioClip.html">AudioClip</a></li><li><a href="module-sound.Microphone.html">Microphone</a></li><li><a href="module-sound.Sound.html">Sound</a></li><li><a href="module-sound.TonePlayer.html">TonePlayer</a></li><li><a href="module-sound.TrackPlayer.html">TrackPlayer</a></li><li><a href="module-sound.Transcriber.html">Transcriber</a></li><li><a href="module-sound.Transcript.html">Transcript</a></li><li><a href="module-util.Clock.html">Clock</a></li><li><a href="module-util.Color.html">Color</a></li><li><a href="module-util.CountdownTimer.html">CountdownTimer</a></li><li><a href="module-util.EventEmitter.html">EventEmitter</a></li><li><a href="module-util.MixinBuilder.html">MixinBuilder</a></li><li><a href="module-util.MonotonicClock.html">MonotonicClock</a></li><li><a href="module-util.PsychObject.html">PsychObject</a></li><li><a href="module-util.Scheduler.html">Scheduler</a></li><li><a href="module-visual.ButtonStim.html">ButtonStim</a></li><li><a href="module-visual.Form.html">Form</a></li><li><a href="module-visual.ImageStim.html">ImageStim</a></li><li><a href="module-visual.MovieStim.html">MovieStim</a></li><li><a href="module-visual.Polygon.html">Polygon</a></li><li><a href="module-visual.Rect.html">Rect</a></li><li><a href="module-visual.ShapeStim.html">ShapeStim</a></li><li><a href="module-visual.Slider.html">Slider</a></li><li><a href="module-visual.TextBox.html">TextBox</a></li><li><a href="module-visual.TextStim.html">TextStim</a></li><li><a href="module-visual.VisualStim.html">VisualStim</a></li></ul><h3>Interfaces</h3><ul><li><a href="module-sound.SoundPlayer.html">SoundPlayer</a></li></ul><h3>Mixins</h3><ul><li><a href="module-core.WindowMixin.html">WindowMixin</a></li><li><a href="module-util.ColorMixin.html">ColorMixin</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.7</a> on Wed Jun 02 2021 13:52:56 GMT+0200 (Central European Summer Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>