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/CLAUDE.md +115 -0
- package/README.md +152 -15
- package/dist/cjs/common-interfaces.d.ts +92 -0
- package/dist/cjs/common-interfaces.js +2 -0
- package/dist/cjs/emotions.d.ts +2 -7
- package/dist/cjs/emotions.js +56 -22
- package/dist/cjs/gonogo.d.ts +1 -6
- package/dist/cjs/gonogo.js +125 -46
- package/dist/cjs/index.d.ts +2 -5
- package/dist/cjs/nback.d.ts +6 -12
- package/dist/cjs/nback.js +159 -15
- package/dist/cjs/spatial-cognition.d.ts +1 -6
- package/dist/cjs/spatial-cognition.js +67 -14
- package/dist/esm/common-interfaces.d.ts +92 -0
- package/dist/esm/common-interfaces.js +1 -0
- package/dist/esm/emotions.d.ts +2 -7
- package/dist/esm/emotions.js +55 -21
- package/dist/esm/gonogo.d.ts +1 -6
- package/dist/esm/gonogo.js +125 -46
- package/dist/esm/index.d.ts +2 -5
- package/dist/esm/nback.d.ts +6 -12
- package/dist/esm/nback.js +155 -15
- package/dist/esm/spatial-cognition.d.ts +1 -6
- package/dist/esm/spatial-cognition.js +67 -14
- package/package.json +2 -2
- package/src/__tests__/integration/emotions.test.ts +122 -14
- package/src/__tests__/integration/gonogo.test.ts +240 -27
- package/src/__tests__/integration/nback-versions.test.ts +182 -0
- package/src/__tests__/integration/nback.test.ts +113 -20
- package/src/__tests__/integration/spatial-cognition.test.ts +106 -1
- package/src/common-interfaces.ts +125 -0
- package/src/emotions.ts +68 -29
- package/src/gonogo.ts +128 -55
- package/src/index.ts +22 -6
- package/src/nback.ts +160 -31
- package/src/spatial-cognition.ts +77 -21
package/src/gonogo.ts
CHANGED
|
@@ -1,76 +1,149 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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 {
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
results
|
|
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
|
-
|
|
53
|
-
|
|
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 };
|
package/src/spatial-cognition.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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[]) => {
|