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