compileprofile-spacetests 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CLAUDE.md +114 -114
  2. package/README.md +171 -147
  3. package/dist/cjs/common-interfaces.d.ts +33 -1
  4. package/dist/cjs/gonogo.js +89 -57
  5. package/dist/cjs/nback.d.ts +6 -2
  6. package/dist/cjs/nback.js +132 -2
  7. package/dist/cjs/spatial-cognition.js +0 -2
  8. package/dist/esm/common-interfaces.d.ts +33 -1
  9. package/dist/esm/gonogo.js +89 -57
  10. package/dist/esm/nback.d.ts +6 -2
  11. package/dist/esm/nback.js +128 -2
  12. package/dist/esm/spatial-cognition.js +0 -2
  13. package/example-data/emotions.json +807 -807
  14. package/example-data/gonogo.json +5392 -5392
  15. package/example-data/nback.json +892 -892
  16. package/example-data/participant.json +6 -6
  17. package/example-data/spatial-cognition.json +462 -462
  18. package/jest.config.js +13 -13
  19. package/original/emotion-recognition.R +39 -39
  20. package/original/gonogo.R +32 -32
  21. package/package.json +40 -40
  22. package/src/__tests__/integration/browserInfo.test.ts +16 -16
  23. package/src/__tests__/integration/emotions.test.ts +172 -172
  24. package/src/__tests__/integration/gonogo.test.ts +293 -183
  25. package/src/__tests__/integration/nback-versions.test.ts +182 -0
  26. package/src/__tests__/integration/nback.test.ts +148 -148
  27. package/src/__tests__/integration/spatial-cognition.test.ts +171 -171
  28. package/src/__tests__/test-utils/testData.ts +79 -79
  29. package/src/common-interfaces.ts +124 -78
  30. package/src/emotions.ts +98 -98
  31. package/src/getters.ts +21 -21
  32. package/src/gonogo.ts +149 -127
  33. package/src/index.ts +39 -39
  34. package/src/nback.ts +182 -61
  35. package/src/spatial-cognition.ts +110 -112
  36. package/tsconfig.cjs.json +12 -12
  37. package/tsconfig.esm.json +14 -14
@@ -1,4 +1,15 @@
1
1
  "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
2
13
  Object.defineProperty(exports, "__esModule", { value: true });
3
14
  exports.gonogo_results = exports.gonogo_extract_trials = exports.gonogo_calculate_results = void 0;
4
15
  var BEST_SCORE = 300;
@@ -9,49 +20,69 @@ var gonogo_calculate_results = function (data) {
9
20
  return results;
10
21
  };
11
22
  exports.gonogo_calculate_results = gonogo_calculate_results;
23
+ // Which stimulus is "go" in each block of the runner:
24
+ // 1 astronaut, 2 alien, 3 rocket, 4 anything but the rocket (the UFOs).
25
+ var should_go_for = function (block_number, stimulus) {
26
+ switch (block_number) {
27
+ case 1: return stimulus == "astronaut_trans_1.png";
28
+ case 2: return stimulus == "alien_trans_1.png";
29
+ case 3: return stimulus == "rocket_trans_1.png";
30
+ case 4: return stimulus != "rocket_trans_1.png";
31
+ default: return false;
32
+ }
33
+ };
12
34
  var gonogo_extract_trials = function (obj) {
13
- // GONOGO_IMAGE_PATH <- "https://unyavwofnmwihnpvkjji.supabase.co/storage/v1/object/public/test-stimuli/gonogo/"
14
- var trials = obj.filter(function (o) { return 'image' in o && o.image !== undefined; });
15
- trials.forEach(function (o) {
16
- // image is in format <stimulus>_trans_<number>.png
17
- // "image": "https://unyavwofnmwihnpvkjji.supabase.co/storage/v1/object/public/test-stimuli/gonogo/aliens/astronaut_trans_1.png",
18
- var match = o.image.match(/([^/]+)_(\d+)\.png$/);
19
- if (match) {
20
- o.stimulus = match[0];
21
- o.stimulustype = match[1];
22
- o.image_number = match[2];
23
- }
24
- else {
35
+ var trials = [];
36
+ obj.filter(function (o) { return 'image' in o && o.image !== undefined && o.image !== null; }).forEach(function (o) {
37
+ // image is in format <stimulus>_trans_<number>.png, e.g.
38
+ // ".../test-stimuli/gonogo/aliens/astronaut_trans_1.png"
39
+ var match = String(o.image).match(/([^/]+)_(\d+)\.png$/);
40
+ if (!match) {
41
+ // Without a recognisable stimulus the trial cannot be scored, so leave it out
42
+ // instead of counting it as neither correct nor incorrect.
25
43
  console.warn('Image format did not match expected pattern:', o.image);
26
44
  return;
27
45
  }
28
- o.should_go = false;
29
- o.did_go = o.response !== null;
30
- switch (o.block_number) {
31
- case 1:
32
- o.should_go = o.stimulus == "astronaut_trans_1.png";
33
- break;
34
- case 2:
35
- o.should_go = o.stimulus == "alien_trans_1.png";
36
- break;
37
- case 3:
38
- o.should_go = o.stimulus == "rocket_trans_1.png";
39
- break;
40
- case 4:
41
- o.should_go = !(o.stimulus == "rocket_trans_1.png");
42
- break;
43
- default:
44
- o.should_go = false;
45
- break;
46
- }
47
- o.correct = o.should_go == o.did_go;
46
+ // Copy, so the caller's data is not modified.
47
+ var trial = __assign({}, o);
48
+ trial.stimulus = match[0];
49
+ trial.stimulustype = match[1];
50
+ trial.image_number = match[2];
51
+ // Prefer a go/no-go flag recorded by the runner; otherwise derive it from block and image.
52
+ trial.should_go = typeof o.should_go === 'boolean' ? o.should_go : should_go_for(o.block_number, trial.stimulus);
53
+ trial.did_go = o.response !== null && o.response !== undefined;
54
+ trial.correct = trial.should_go == trial.did_go;
55
+ trials.push(trial);
48
56
  });
49
57
  return trials;
50
58
  };
51
59
  exports.gonogo_extract_trials = gonogo_extract_trials;
52
- // TODO this
53
- var calculate_performance_score = function (avg_rt, avg_acc) {
54
- var performance = avg_rt / (Math.pow(avg_acc, 5) + 0.001);
60
+ var has_rt = function (o) { return o.did_go === true && typeof o.rt === 'number' && !isNaN(o.rt); };
61
+ // Mean of the values; 0 for an empty list, so callers always get a number.
62
+ var mean = function (values) { return values.length > 0 ? values.reduce(function (a, b) { return a + b; }, 0) / values.length : 0; };
63
+ // Mean RT over the trials that have a response; trials without one have no RT and are not averaged in as 0.
64
+ var mean_rt = function (trials) { return mean(trials.filter(has_rt).map(function (o) { return o.rt; })); };
65
+ var proportion = function (trials, pred) {
66
+ return trials.length > 0 ? trials.filter(pred).length / trials.length : 0;
67
+ };
68
+ // Hit rate and correct-rejection rate weighted equally. When a set has no go (or no no-go)
69
+ // trials, the other rate is used alone.
70
+ var balanced_accuracy = function (go_trials, nogo_trials) {
71
+ var rates = [];
72
+ if (go_trials.length > 0)
73
+ rates.push(proportion(go_trials, function (o) { return o.correct === true; }));
74
+ if (nogo_trials.length > 0)
75
+ rates.push(proportion(nogo_trials, function (o) { return o.correct === true; }));
76
+ return mean(rates);
77
+ };
78
+ // Score 0-100 from the hit RT and the balanced accuracy: RT / (acc^5), clamped to 300-5000 ms.
79
+ // Without any hit there is no RT to score, so the score is 0. (Previously an RT of 0 was
80
+ // clamped to the best value, so a participant who never responded scored 100.)
81
+ var calculate_performance_score = function (hit_rt, acc) {
82
+ if (!(hit_rt > 0)) {
83
+ return 0;
84
+ }
85
+ var performance = hit_rt / (Math.pow(acc, 5) + 0.001);
55
86
  performance = Math.min(Math.max(performance, BEST_SCORE), WORST_SCORE);
56
87
  var performance_score = (1 - ((performance - BEST_SCORE) / (WORST_SCORE - BEST_SCORE))) * 100;
57
88
  return performance_score;
@@ -63,51 +94,52 @@ var gonogo_results = function (data) {
63
94
  }
64
95
  var correct_trials = data.filter(function (o) { return o.correct === true; });
65
96
  var incorrect_trials = data.filter(function (o) { return o.correct === false; });
66
- var missing_trials = []; // technically not possible in GNG, as no response is just incorrect
67
97
  var go_trials = data.filter(function (o) { return o.should_go === true; });
68
98
  var nogo_trials = data.filter(function (o) { return o.should_go === false; });
69
- var total_rt = data.reduce(function (a, b) { return a + (b.rt || 0); }, 0);
70
- var correct_rt = correct_trials.reduce(function (a, b) { return a + (b.rt || 0); }, 0);
71
- var incorrect_rt = incorrect_trials.reduce(function (a, b) { return a + (b.rt || 0); }, 0);
72
- var responded_trials = data.filter(function (o) { return o.response !== null; });
73
- var correct_responded_trials = responded_trials.filter(function (o) { return o.correct === true; });
74
- var incorrect_responded_trials = responded_trials.filter(function (o) { return o.correct === false; });
75
- var avg_rt = responded_trials.length > 0 ? total_rt / responded_trials.length : 0;
76
- var avg_acc = correct_trials.length / data.length;
77
- // Calculate performance score using the existing formula
78
- var performance_score = calculate_performance_score(avg_rt, avg_acc);
99
+ // A response is correct on go trials (hit) and incorrect on no-go trials (false alarm).
100
+ // Correct no-go trials and missed go trials have no response, so no RT.
101
+ var hit_rt = mean_rt(go_trials);
102
+ var balanced_acc = balanced_accuracy(go_trials, nogo_trials);
79
103
  var common = {
80
104
  n_trials: data.length,
81
105
  n_correct_trials: correct_trials.length,
82
106
  n_incorrect_trials: incorrect_trials.length,
83
- avg_rt_total: avg_rt,
84
- avg_rt_correct: correct_trials.length > 0 ? correct_rt / correct_responded_trials.length : 0,
85
- avg_rt_incorrect: incorrect_trials.length > 0 ? incorrect_rt / incorrect_responded_trials.length : 0,
86
- performance_score: performance_score
107
+ avg_rt_total: mean_rt(data),
108
+ avg_rt_correct: mean_rt(correct_trials),
109
+ avg_rt_incorrect: mean_rt(incorrect_trials),
110
+ performance_score: calculate_performance_score(hit_rt, balanced_acc)
87
111
  };
88
112
  // Group by block for special results
89
113
  var by_block = Object.values(data.reduce(function (acc, trial) {
90
114
  var key = trial.block_number || 'unknown';
91
115
  if (!acc[key])
92
- acc[key] = { block: key, trials: [], avg_rt: 0, avg_acc: 0 };
116
+ acc[key] = { block: key, trials: [] };
93
117
  acc[key].trials.push(trial);
94
118
  return acc;
95
119
  }, {})).map(function (group) {
96
- group.avg_rt = group.trials.reduce(function (a, b) { return a + (b.rt || 0); }, 0) / group.trials.length;
97
- group.avg_acc = group.trials.reduce(function (a, b) { return a + (b.correct ? 1 : 0); }, 0) / group.trials.length;
120
+ var block_go = group.trials.filter(function (o) { return o.should_go === true; });
121
+ var block_nogo = group.trials.filter(function (o) { return o.should_go === false; });
122
+ group.avg_rt = mean_rt(block_go);
123
+ group.avg_acc = proportion(group.trials, function (o) { return o.correct === true; });
124
+ group.hit_rate = proportion(block_go, function (o) { return o.correct === true; });
125
+ group.correct_rejection_rate = proportion(block_nogo, function (o) { return o.correct === true; });
126
+ group.balanced_acc = balanced_accuracy(block_go, block_nogo);
98
127
  return group;
99
128
  });
100
129
  var special = {
130
+ balanced_acc: balanced_acc,
101
131
  by_block: by_block,
102
132
  go_trials: {
103
133
  n: go_trials.length,
104
- avg_rt: go_trials.length > 0 ? go_trials.reduce(function (a, b) { return a + (b.rt || 0); }, 0) / go_trials.length : 0,
105
- avg_acc: go_trials.length > 0 ? go_trials.reduce(function (a, b) { return a + (b.correct ? 1 : 0); }, 0) / go_trials.length : 0,
134
+ avg_rt: hit_rt,
135
+ avg_acc: proportion(go_trials, function (o) { return o.correct === true; }),
136
+ n_omissions: go_trials.filter(function (o) { return o.did_go === false; }).length,
106
137
  },
107
138
  nogo_trials: {
108
139
  n: nogo_trials.length,
109
- avg_rt: nogo_trials.length > 0 ? nogo_trials.reduce(function (a, b) { return a + (b.rt || 0); }, 0) / nogo_trials.length : 0,
110
- avg_acc: nogo_trials.length > 0 ? nogo_trials.reduce(function (a, b) { return a + (b.correct ? 1 : 0); }, 0) / nogo_trials.length : 0,
140
+ avg_rt: mean_rt(nogo_trials),
141
+ avg_acc: proportion(nogo_trials, function (o) { return o.correct === true; }),
142
+ n_commissions: nogo_trials.filter(function (o) { return o.did_go === true; }).length,
111
143
  }
112
144
  };
113
145
  return {
@@ -1,5 +1,9 @@
1
- import { NbackResultsStructure } from './common-interfaces';
1
+ import { NbackResultsStructure, NbackGroupResults, NbackBlockResults, NbackLevelResults } from './common-interfaces';
2
2
  declare const nback_calculate_results: (obj: any[]) => NbackResultsStructure;
3
3
  declare const nback_extract_trials: (obj: any[]) => any[];
4
4
  declare const nback_results: (trials: any[]) => NbackResultsStructure;
5
- export { nback_extract_trials, nback_results, nback_calculate_results, NbackResultsStructure };
5
+ declare const inverse_normal: (p: number) => number;
6
+ declare const nback_group_results: (trials: any[]) => NbackGroupResults;
7
+ declare const nback_results_by_block: (trials: any[]) => NbackBlockResults[];
8
+ declare const nback_results_by_nback: (trials: any[]) => NbackLevelResults[];
9
+ export { nback_extract_trials, nback_results, nback_calculate_results, nback_group_results, nback_results_by_block, nback_results_by_nback, inverse_normal, NbackResultsStructure };
package/dist/cjs/nback.js CHANGED
@@ -1,12 +1,33 @@
1
1
  "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
2
13
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.nback_calculate_results = exports.nback_results = exports.nback_extract_trials = void 0;
14
+ exports.inverse_normal = exports.nback_results_by_nback = exports.nback_results_by_block = exports.nback_group_results = exports.nback_calculate_results = exports.nback_results = exports.nback_extract_trials = void 0;
4
15
  // from the passed json extract those objects that have field is_target
16
+ // Version 1 trials have no n_back field and get exactly the original result. Version 2 trials
17
+ // (n_back, block, block_index on each trial) also get special.by_block and special.by_nback.
5
18
  var nback_calculate_results = function (obj) {
6
19
  var trials = nback_extract_trials(obj);
7
- return nback_results(trials);
20
+ var results = nback_results(trials);
21
+ if (nback_is_versioned(trials)) {
22
+ results.special.by_block = nback_results_by_block(trials);
23
+ results.special.by_nback = nback_results_by_nback(trials);
24
+ }
25
+ return results;
8
26
  };
9
27
  exports.nback_calculate_results = nback_calculate_results;
28
+ var nback_is_versioned = function (trials) {
29
+ return trials.some(function (t) { return t.n_back !== undefined && t.n_back !== null; });
30
+ };
10
31
  var nback_extract_trials = function (obj) {
11
32
  var trials = obj.filter(function (o) { return 'is_target' in o; });
12
33
  return trials;
@@ -52,3 +73,112 @@ var nback_results = function (trials) {
52
73
  return results;
53
74
  };
54
75
  exports.nback_results = nback_results;
76
+ // Standard normal CDF (Marsaglia 2004, Taylor series; absolute error ~1e-15 for |x| < 8)
77
+ var normal_cdf = function (x) {
78
+ var sum = x, term = x, prev = 0;
79
+ var x2 = x * x;
80
+ for (var i = 3; sum !== prev; i += 2) {
81
+ prev = sum;
82
+ term *= x2 / i;
83
+ sum = prev + term;
84
+ }
85
+ return 0.5 + sum * Math.exp(-0.5 * x2 - 0.91893853320467274178);
86
+ };
87
+ // Inverse of the standard normal CDF: Acklam's rational approximation, polished with one
88
+ // Halley step against normal_cdf, as Acklam recommends. Accurate to ~1e-12 for 1e-10 < p < 1 - 1e-10.
89
+ var inverse_normal = function (p) {
90
+ if (p <= 0)
91
+ return -Infinity;
92
+ if (p >= 1)
93
+ return Infinity;
94
+ var x = acklam_inverse_normal(p);
95
+ var e = normal_cdf(x) - p;
96
+ var u = e * Math.sqrt(2 * Math.PI) * Math.exp(x * x / 2);
97
+ return x - u / (1 + x * u / 2);
98
+ };
99
+ exports.inverse_normal = inverse_normal;
100
+ var acklam_inverse_normal = function (p) {
101
+ var a = [-3.969683028665376e+01, 2.209460984245205e+02, -2.759285104469687e+02,
102
+ 1.383577518672690e+02, -3.066479806614716e+01, 2.506628277459239e+00];
103
+ var b = [-5.447609879822406e+01, 1.615858368580409e+02, -1.556989798598866e+02,
104
+ 6.680131188771972e+01, -1.328068155288572e+01];
105
+ var c = [-7.784894002430293e-03, -3.223964580411365e-01, -2.400758277161838e+00,
106
+ -2.549671010322563e+00, 4.374664141464968e+00, 2.938163982698783e+00];
107
+ var d = [7.784695709041462e-03, 3.224671290700398e-01, 2.445134137142996e+00,
108
+ 3.754408661907416e+00];
109
+ var p_low = 0.02425;
110
+ if (p < p_low) {
111
+ var q_1 = Math.sqrt(-2 * Math.log(p));
112
+ return (((((c[0] * q_1 + c[1]) * q_1 + c[2]) * q_1 + c[3]) * q_1 + c[4]) * q_1 + c[5]) /
113
+ ((((d[0] * q_1 + d[1]) * q_1 + d[2]) * q_1 + d[3]) * q_1 + 1);
114
+ }
115
+ if (p > 1 - p_low)
116
+ return -acklam_inverse_normal(1 - p);
117
+ var q = p - 0.5;
118
+ var r = q * q;
119
+ return (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q /
120
+ (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1);
121
+ };
122
+ var mean_or_null = function (xs) {
123
+ return xs.length > 0 ? xs.reduce(function (s, x) { return s + x; }, 0) / xs.length : null;
124
+ };
125
+ var responded = function (t) { return t.response !== null && t.response !== undefined; };
126
+ // d' uses the log-linear correction (Hautus 1995), so it stays finite at 0 % or 100 % rates
127
+ var nback_group_results = function (trials) {
128
+ var targets = trials.filter(function (t) { return t.is_target; });
129
+ var non_targets = trials.filter(function (t) { return !t.is_target; });
130
+ var hits = targets.filter(responded);
131
+ var false_alarms = non_targets.filter(responded);
132
+ var lures = non_targets.filter(function (t) { return t.is_lure === true; });
133
+ var lure_false_alarms = lures.filter(responded).length;
134
+ var n_correct = trials.filter(function (t) { return t.correct === true; }).length;
135
+ var rts = function (ts) { return ts.map(function (t) { return t.rt; }).filter(function (rt) { return typeof rt === 'number'; }); };
136
+ return {
137
+ n_trials: trials.length,
138
+ n_correct_trials: n_correct,
139
+ performance_score: trials.length > 0 ? n_correct * 100 / trials.length : 0,
140
+ n_targets: targets.length,
141
+ n_non_targets: non_targets.length,
142
+ hits: hits.length,
143
+ misses: targets.length - hits.length,
144
+ false_alarms: false_alarms.length,
145
+ correct_rejections: non_targets.length - false_alarms.length,
146
+ hit_rate: targets.length > 0 ? hits.length / targets.length : null,
147
+ false_alarm_rate: non_targets.length > 0 ? false_alarms.length / non_targets.length : null,
148
+ d_prime: targets.length > 0 && non_targets.length > 0
149
+ ? inverse_normal((hits.length + 0.5) / (targets.length + 1))
150
+ - inverse_normal((false_alarms.length + 0.5) / (non_targets.length + 1))
151
+ : null,
152
+ n_lures: lures.length,
153
+ lure_false_alarms: lure_false_alarms,
154
+ lure_false_alarm_rate: lures.length > 0 ? lure_false_alarms / lures.length : null,
155
+ avg_rt_hits: mean_or_null(rts(hits)),
156
+ avg_rt_false_alarms: mean_or_null(rts(false_alarms)),
157
+ };
158
+ };
159
+ exports.nback_group_results = nback_group_results;
160
+ var nback_results_by_block = function (trials) {
161
+ var blocks = new Map();
162
+ for (var _i = 0, trials_1 = trials; _i < trials_1.length; _i++) {
163
+ var t = trials_1[_i];
164
+ if (!blocks.has(t.block_index))
165
+ blocks.set(t.block_index, []);
166
+ blocks.get(t.block_index).push(t);
167
+ }
168
+ return Array.from(blocks.entries())
169
+ .sort(function (_a, _b) {
170
+ var a = _a[0];
171
+ var b = _b[0];
172
+ return a - b;
173
+ })
174
+ .map(function (_a) {
175
+ var block_index = _a[0], ts = _a[1];
176
+ return (__assign({ block: ts[0].block, block_index: block_index, n_back: ts[0].n_back }, nback_group_results(ts)));
177
+ });
178
+ };
179
+ exports.nback_results_by_block = nback_results_by_block;
180
+ var nback_results_by_nback = function (trials) {
181
+ var levels = Array.from(new Set(trials.map(function (t) { return t.n_back; }))).sort(function (a, b) { return a - b; });
182
+ return levels.map(function (n_back) { return (__assign({ n_back: n_back }, nback_group_results(trials.filter(function (t) { return t.n_back === n_back; })))); });
183
+ };
184
+ exports.nback_results_by_nback = nback_results_by_nback;
@@ -24,11 +24,9 @@ exports.spatial_extract_trials = spatial_extract_trials;
24
24
  var spatial_results = function (obj) {
25
25
  var correct_trials = obj.filter(function (o) { return o.correct === 1 || o.correct === true; });
26
26
  var incorrect_trials = obj.filter(function (o) { return o.correct === 0 || o.correct === false; });
27
- var missing_trials = obj.filter(function (o) { return o.response === null || o.response === undefined; });
28
27
  var total_rt = obj.reduce(function (a, b) { return a + (b.rt || 0); }, 0);
29
28
  var correct_rt = correct_trials.reduce(function (a, b) { return a + (b.rt || 0); }, 0);
30
29
  var incorrect_rt = incorrect_trials.reduce(function (a, b) { return a + (b.rt || 0); }, 0);
31
- var missing_rt = missing_trials.reduce(function (a, b) { return a + (b.rt || 0); }, 0);
32
30
  var avg_rt = obj.length > 0 ? total_rt / obj.length : 0;
33
31
  var avg_acc = obj.length > 0 ? correct_trials.length / obj.length : 0;
34
32
  var performance_score = calculate_performance_score(avg_rt, avg_acc);
@@ -7,6 +7,33 @@ interface CommonResults {
7
7
  avg_rt_incorrect: number;
8
8
  performance_score: number;
9
9
  }
10
+ interface NbackGroupResults {
11
+ n_trials: number;
12
+ n_correct_trials: number;
13
+ performance_score: number;
14
+ n_targets: number;
15
+ n_non_targets: number;
16
+ hits: number;
17
+ misses: number;
18
+ false_alarms: number;
19
+ correct_rejections: number;
20
+ hit_rate: number | null;
21
+ false_alarm_rate: number | null;
22
+ d_prime: number | null;
23
+ n_lures: number;
24
+ lure_false_alarms: number;
25
+ lure_false_alarm_rate: number | null;
26
+ avg_rt_hits: number | null;
27
+ avg_rt_false_alarms: number | null;
28
+ }
29
+ interface NbackBlockResults extends NbackGroupResults {
30
+ block: string;
31
+ block_index: number;
32
+ n_back: number;
33
+ }
34
+ interface NbackLevelResults extends NbackGroupResults {
35
+ n_back: number;
36
+ }
10
37
  interface NbackSpecialResults {
11
38
  target_trials: {
12
39
  n: number;
@@ -18,6 +45,8 @@ interface NbackSpecialResults {
18
45
  avg_rt: number;
19
46
  avg_acc: number;
20
47
  };
48
+ by_block?: NbackBlockResults[];
49
+ by_nback?: NbackLevelResults[];
21
50
  }
22
51
  interface SpatialSpecialResults {
23
52
  by_distance: any[];
@@ -25,16 +54,19 @@ interface SpatialSpecialResults {
25
54
  by_rotation: any[];
26
55
  }
27
56
  interface GonogoSpecialResults {
57
+ balanced_acc: number;
28
58
  by_block: any[];
29
59
  go_trials: {
30
60
  n: number;
31
61
  avg_rt: number;
32
62
  avg_acc: number;
63
+ n_omissions: number;
33
64
  };
34
65
  nogo_trials: {
35
66
  n: number;
36
67
  avg_rt: number;
37
68
  avg_acc: number;
69
+ n_commissions: number;
38
70
  };
39
71
  }
40
72
  interface EmotionSpecialResults {
@@ -57,4 +89,4 @@ interface EmotionResultsStructure {
57
89
  common: CommonResults;
58
90
  special: EmotionSpecialResults;
59
91
  }
60
- export { CommonResults, NbackResultsStructure, SpatialResultsStructure, GonogoResultsStructure, EmotionResultsStructure, NbackSpecialResults, SpatialSpecialResults, GonogoSpecialResults, EmotionSpecialResults };
92
+ export { CommonResults, NbackResultsStructure, SpatialResultsStructure, GonogoResultsStructure, EmotionResultsStructure, NbackSpecialResults, NbackGroupResults, NbackBlockResults, NbackLevelResults, SpatialSpecialResults, GonogoSpecialResults, EmotionSpecialResults };
@@ -1,3 +1,14 @@
1
+ var __assign = (this && this.__assign) || function () {
2
+ __assign = Object.assign || function(t) {
3
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4
+ s = arguments[i];
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6
+ t[p] = s[p];
7
+ }
8
+ return t;
9
+ };
10
+ return __assign.apply(this, arguments);
11
+ };
1
12
  var BEST_SCORE = 300;
2
13
  var WORST_SCORE = 5000;
3
14
  var gonogo_calculate_results = function (data) {
@@ -5,48 +16,68 @@ var gonogo_calculate_results = function (data) {
5
16
  var results = gonogo_results(trials);
6
17
  return results;
7
18
  };
19
+ // Which stimulus is "go" in each block of the runner:
20
+ // 1 astronaut, 2 alien, 3 rocket, 4 anything but the rocket (the UFOs).
21
+ var should_go_for = function (block_number, stimulus) {
22
+ switch (block_number) {
23
+ case 1: return stimulus == "astronaut_trans_1.png";
24
+ case 2: return stimulus == "alien_trans_1.png";
25
+ case 3: return stimulus == "rocket_trans_1.png";
26
+ case 4: return stimulus != "rocket_trans_1.png";
27
+ default: return false;
28
+ }
29
+ };
8
30
  var gonogo_extract_trials = function (obj) {
9
- // GONOGO_IMAGE_PATH <- "https://unyavwofnmwihnpvkjji.supabase.co/storage/v1/object/public/test-stimuli/gonogo/"
10
- var trials = obj.filter(function (o) { return 'image' in o && o.image !== undefined; });
11
- trials.forEach(function (o) {
12
- // image is in format <stimulus>_trans_<number>.png
13
- // "image": "https://unyavwofnmwihnpvkjji.supabase.co/storage/v1/object/public/test-stimuli/gonogo/aliens/astronaut_trans_1.png",
14
- var match = o.image.match(/([^/]+)_(\d+)\.png$/);
15
- if (match) {
16
- o.stimulus = match[0];
17
- o.stimulustype = match[1];
18
- o.image_number = match[2];
19
- }
20
- else {
31
+ var trials = [];
32
+ obj.filter(function (o) { return 'image' in o && o.image !== undefined && o.image !== null; }).forEach(function (o) {
33
+ // image is in format <stimulus>_trans_<number>.png, e.g.
34
+ // ".../test-stimuli/gonogo/aliens/astronaut_trans_1.png"
35
+ var match = String(o.image).match(/([^/]+)_(\d+)\.png$/);
36
+ if (!match) {
37
+ // Without a recognisable stimulus the trial cannot be scored, so leave it out
38
+ // instead of counting it as neither correct nor incorrect.
21
39
  console.warn('Image format did not match expected pattern:', o.image);
22
40
  return;
23
41
  }
24
- o.should_go = false;
25
- o.did_go = o.response !== null;
26
- switch (o.block_number) {
27
- case 1:
28
- o.should_go = o.stimulus == "astronaut_trans_1.png";
29
- break;
30
- case 2:
31
- o.should_go = o.stimulus == "alien_trans_1.png";
32
- break;
33
- case 3:
34
- o.should_go = o.stimulus == "rocket_trans_1.png";
35
- break;
36
- case 4:
37
- o.should_go = !(o.stimulus == "rocket_trans_1.png");
38
- break;
39
- default:
40
- o.should_go = false;
41
- break;
42
- }
43
- o.correct = o.should_go == o.did_go;
42
+ // Copy, so the caller's data is not modified.
43
+ var trial = __assign({}, o);
44
+ trial.stimulus = match[0];
45
+ trial.stimulustype = match[1];
46
+ trial.image_number = match[2];
47
+ // Prefer a go/no-go flag recorded by the runner; otherwise derive it from block and image.
48
+ trial.should_go = typeof o.should_go === 'boolean' ? o.should_go : should_go_for(o.block_number, trial.stimulus);
49
+ trial.did_go = o.response !== null && o.response !== undefined;
50
+ trial.correct = trial.should_go == trial.did_go;
51
+ trials.push(trial);
44
52
  });
45
53
  return trials;
46
54
  };
47
- // TODO this
48
- var calculate_performance_score = function (avg_rt, avg_acc) {
49
- var performance = avg_rt / (Math.pow(avg_acc, 5) + 0.001);
55
+ var has_rt = function (o) { return o.did_go === true && typeof o.rt === 'number' && !isNaN(o.rt); };
56
+ // Mean of the values; 0 for an empty list, so callers always get a number.
57
+ var mean = function (values) { return values.length > 0 ? values.reduce(function (a, b) { return a + b; }, 0) / values.length : 0; };
58
+ // Mean RT over the trials that have a response; trials without one have no RT and are not averaged in as 0.
59
+ var mean_rt = function (trials) { return mean(trials.filter(has_rt).map(function (o) { return o.rt; })); };
60
+ var proportion = function (trials, pred) {
61
+ return trials.length > 0 ? trials.filter(pred).length / trials.length : 0;
62
+ };
63
+ // Hit rate and correct-rejection rate weighted equally. When a set has no go (or no no-go)
64
+ // trials, the other rate is used alone.
65
+ var balanced_accuracy = function (go_trials, nogo_trials) {
66
+ var rates = [];
67
+ if (go_trials.length > 0)
68
+ rates.push(proportion(go_trials, function (o) { return o.correct === true; }));
69
+ if (nogo_trials.length > 0)
70
+ rates.push(proportion(nogo_trials, function (o) { return o.correct === true; }));
71
+ return mean(rates);
72
+ };
73
+ // Score 0-100 from the hit RT and the balanced accuracy: RT / (acc^5), clamped to 300-5000 ms.
74
+ // Without any hit there is no RT to score, so the score is 0. (Previously an RT of 0 was
75
+ // clamped to the best value, so a participant who never responded scored 100.)
76
+ var calculate_performance_score = function (hit_rt, acc) {
77
+ if (!(hit_rt > 0)) {
78
+ return 0;
79
+ }
80
+ var performance = hit_rt / (Math.pow(acc, 5) + 0.001);
50
81
  performance = Math.min(Math.max(performance, BEST_SCORE), WORST_SCORE);
51
82
  var performance_score = (1 - ((performance - BEST_SCORE) / (WORST_SCORE - BEST_SCORE))) * 100;
52
83
  return performance_score;
@@ -58,51 +89,52 @@ var gonogo_results = function (data) {
58
89
  }
59
90
  var correct_trials = data.filter(function (o) { return o.correct === true; });
60
91
  var incorrect_trials = data.filter(function (o) { return o.correct === false; });
61
- var missing_trials = []; // technically not possible in GNG, as no response is just incorrect
62
92
  var go_trials = data.filter(function (o) { return o.should_go === true; });
63
93
  var nogo_trials = data.filter(function (o) { return o.should_go === false; });
64
- var total_rt = data.reduce(function (a, b) { return a + (b.rt || 0); }, 0);
65
- var correct_rt = correct_trials.reduce(function (a, b) { return a + (b.rt || 0); }, 0);
66
- var incorrect_rt = incorrect_trials.reduce(function (a, b) { return a + (b.rt || 0); }, 0);
67
- var responded_trials = data.filter(function (o) { return o.response !== null; });
68
- var correct_responded_trials = responded_trials.filter(function (o) { return o.correct === true; });
69
- var incorrect_responded_trials = responded_trials.filter(function (o) { return o.correct === false; });
70
- var avg_rt = responded_trials.length > 0 ? total_rt / responded_trials.length : 0;
71
- var avg_acc = correct_trials.length / data.length;
72
- // Calculate performance score using the existing formula
73
- var performance_score = calculate_performance_score(avg_rt, avg_acc);
94
+ // A response is correct on go trials (hit) and incorrect on no-go trials (false alarm).
95
+ // Correct no-go trials and missed go trials have no response, so no RT.
96
+ var hit_rt = mean_rt(go_trials);
97
+ var balanced_acc = balanced_accuracy(go_trials, nogo_trials);
74
98
  var common = {
75
99
  n_trials: data.length,
76
100
  n_correct_trials: correct_trials.length,
77
101
  n_incorrect_trials: incorrect_trials.length,
78
- avg_rt_total: avg_rt,
79
- avg_rt_correct: correct_trials.length > 0 ? correct_rt / correct_responded_trials.length : 0,
80
- avg_rt_incorrect: incorrect_trials.length > 0 ? incorrect_rt / incorrect_responded_trials.length : 0,
81
- performance_score: performance_score
102
+ avg_rt_total: mean_rt(data),
103
+ avg_rt_correct: mean_rt(correct_trials),
104
+ avg_rt_incorrect: mean_rt(incorrect_trials),
105
+ performance_score: calculate_performance_score(hit_rt, balanced_acc)
82
106
  };
83
107
  // Group by block for special results
84
108
  var by_block = Object.values(data.reduce(function (acc, trial) {
85
109
  var key = trial.block_number || 'unknown';
86
110
  if (!acc[key])
87
- acc[key] = { block: key, trials: [], avg_rt: 0, avg_acc: 0 };
111
+ acc[key] = { block: key, trials: [] };
88
112
  acc[key].trials.push(trial);
89
113
  return acc;
90
114
  }, {})).map(function (group) {
91
- group.avg_rt = group.trials.reduce(function (a, b) { return a + (b.rt || 0); }, 0) / group.trials.length;
92
- group.avg_acc = group.trials.reduce(function (a, b) { return a + (b.correct ? 1 : 0); }, 0) / group.trials.length;
115
+ var block_go = group.trials.filter(function (o) { return o.should_go === true; });
116
+ var block_nogo = group.trials.filter(function (o) { return o.should_go === false; });
117
+ group.avg_rt = mean_rt(block_go);
118
+ group.avg_acc = proportion(group.trials, function (o) { return o.correct === true; });
119
+ group.hit_rate = proportion(block_go, function (o) { return o.correct === true; });
120
+ group.correct_rejection_rate = proportion(block_nogo, function (o) { return o.correct === true; });
121
+ group.balanced_acc = balanced_accuracy(block_go, block_nogo);
93
122
  return group;
94
123
  });
95
124
  var special = {
125
+ balanced_acc: balanced_acc,
96
126
  by_block: by_block,
97
127
  go_trials: {
98
128
  n: go_trials.length,
99
- avg_rt: go_trials.length > 0 ? go_trials.reduce(function (a, b) { return a + (b.rt || 0); }, 0) / go_trials.length : 0,
100
- avg_acc: go_trials.length > 0 ? go_trials.reduce(function (a, b) { return a + (b.correct ? 1 : 0); }, 0) / go_trials.length : 0,
129
+ avg_rt: hit_rt,
130
+ avg_acc: proportion(go_trials, function (o) { return o.correct === true; }),
131
+ n_omissions: go_trials.filter(function (o) { return o.did_go === false; }).length,
101
132
  },
102
133
  nogo_trials: {
103
134
  n: nogo_trials.length,
104
- avg_rt: nogo_trials.length > 0 ? nogo_trials.reduce(function (a, b) { return a + (b.rt || 0); }, 0) / nogo_trials.length : 0,
105
- avg_acc: nogo_trials.length > 0 ? nogo_trials.reduce(function (a, b) { return a + (b.correct ? 1 : 0); }, 0) / nogo_trials.length : 0,
135
+ avg_rt: mean_rt(nogo_trials),
136
+ avg_acc: proportion(nogo_trials, function (o) { return o.correct === true; }),
137
+ n_commissions: nogo_trials.filter(function (o) { return o.did_go === true; }).length,
106
138
  }
107
139
  };
108
140
  return {
@@ -1,5 +1,9 @@
1
- import { NbackResultsStructure } from './common-interfaces';
1
+ import { NbackResultsStructure, NbackGroupResults, NbackBlockResults, NbackLevelResults } from './common-interfaces';
2
2
  declare const nback_calculate_results: (obj: any[]) => NbackResultsStructure;
3
3
  declare const nback_extract_trials: (obj: any[]) => any[];
4
4
  declare const nback_results: (trials: any[]) => NbackResultsStructure;
5
- export { nback_extract_trials, nback_results, nback_calculate_results, NbackResultsStructure };
5
+ declare const inverse_normal: (p: number) => number;
6
+ declare const nback_group_results: (trials: any[]) => NbackGroupResults;
7
+ declare const nback_results_by_block: (trials: any[]) => NbackBlockResults[];
8
+ declare const nback_results_by_nback: (trials: any[]) => NbackLevelResults[];
9
+ export { nback_extract_trials, nback_results, nback_calculate_results, nback_group_results, nback_results_by_block, nback_results_by_nback, inverse_normal, NbackResultsStructure };