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.
- package/CLAUDE.md +114 -114
- package/README.md +171 -147
- package/dist/cjs/common-interfaces.d.ts +33 -1
- package/dist/cjs/gonogo.js +89 -57
- package/dist/cjs/nback.d.ts +6 -2
- package/dist/cjs/nback.js +132 -2
- package/dist/cjs/spatial-cognition.js +0 -2
- package/dist/esm/common-interfaces.d.ts +33 -1
- package/dist/esm/gonogo.js +89 -57
- package/dist/esm/nback.d.ts +6 -2
- package/dist/esm/nback.js +128 -2
- package/dist/esm/spatial-cognition.js +0 -2
- package/example-data/emotions.json +807 -807
- package/example-data/gonogo.json +5392 -5392
- package/example-data/nback.json +892 -892
- package/example-data/participant.json +6 -6
- package/example-data/spatial-cognition.json +462 -462
- package/jest.config.js +13 -13
- package/original/emotion-recognition.R +39 -39
- package/original/gonogo.R +32 -32
- package/package.json +40 -40
- package/src/__tests__/integration/browserInfo.test.ts +16 -16
- package/src/__tests__/integration/emotions.test.ts +172 -172
- package/src/__tests__/integration/gonogo.test.ts +293 -183
- package/src/__tests__/integration/nback-versions.test.ts +182 -0
- package/src/__tests__/integration/nback.test.ts +148 -148
- package/src/__tests__/integration/spatial-cognition.test.ts +171 -171
- package/src/__tests__/test-utils/testData.ts +79 -79
- package/src/common-interfaces.ts +124 -78
- package/src/emotions.ts +98 -98
- package/src/getters.ts +21 -21
- package/src/gonogo.ts +149 -127
- package/src/index.ts +39 -39
- package/src/nback.ts +182 -61
- package/src/spatial-cognition.ts +110 -112
- package/tsconfig.cjs.json +12 -12
- package/tsconfig.esm.json +14 -14
|
@@ -1,184 +1,294 @@
|
|
|
1
|
-
import { gonogo_extract_trials, gonogo_results, gonogo_calculate_results } from '../../gonogo';
|
|
2
|
-
import { loadTestData } from '../test-utils/testData';
|
|
3
|
-
|
|
4
|
-
describe('Gonogo test module', () => {
|
|
5
|
-
// If gonogo data is available in your test data, load it
|
|
6
|
-
// const gonogoData = loadTestData('gonogo').gonogo;
|
|
7
|
-
|
|
8
|
-
const gonogoData = loadTestData('gonogo').gonogo;
|
|
9
|
-
|
|
10
|
-
// Comprehensive mock data for predictable testing
|
|
11
|
-
const mockGonogoData = [
|
|
12
|
-
{
|
|
13
|
-
"trial_index": 8,
|
|
14
|
-
"block_number": 1,
|
|
15
|
-
"image": "https://unyavwofnmwihnpvkjji.supabase.co/storage/v1/object/public/test-stimuli/gonogo/aliens/astronaut_trans_1.png",
|
|
16
|
-
"rt": 1000,
|
|
17
|
-
"stimulus": "",
|
|
18
|
-
"response": 0,
|
|
19
|
-
"trial_type": "html-button-response",
|
|
20
|
-
"plugin_version": "2.0.0",
|
|
21
|
-
"time_elapsed": 15801,
|
|
22
|
-
"attempted": false
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"rt": null,
|
|
26
|
-
"stimulus": "<img src=\"https://unyavwofnmwihnpvkjji.supabase.co/storage/v1/object/public/test-stimuli/fixation_cross.png\" class=\"max-w-none\" style=\"display:none\" width=\"512\" height=\"512\" alt=\"Mapa\">",
|
|
27
|
-
"response": null,
|
|
28
|
-
"trial_type": "html-button-response",
|
|
29
|
-
"trial_index": 9,
|
|
30
|
-
"plugin_version": "2.0.0",
|
|
31
|
-
"time_elapsed": 16335,
|
|
32
|
-
"attempted": false
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"trial_index": 16,
|
|
36
|
-
"block_number": 1,
|
|
37
|
-
"image": "https://unyavwofnmwihnpvkjji.supabase.co/storage/v1/object/public/test-stimuli/gonogo/aliens/alien_trans_1.png",
|
|
38
|
-
"rt": null,
|
|
39
|
-
"stimulus": "",
|
|
40
|
-
"response": null,
|
|
41
|
-
"trial_type": "html-button-response",
|
|
42
|
-
"plugin_version": "2.0.0",
|
|
43
|
-
"time_elapsed": 20290,
|
|
44
|
-
"attempted": false
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
"trial_index": 10,
|
|
48
|
-
"block_number": 1,
|
|
49
|
-
"image": "https://unyavwofnmwihnpvkjji.supabase.co/storage/v1/object/public/test-stimuli/gonogo/aliens/astronaut_trans_1.png",
|
|
50
|
-
"rt": null,
|
|
51
|
-
"stimulus": "",
|
|
52
|
-
"response": null,
|
|
53
|
-
"trial_type": "html-button-response",
|
|
54
|
-
"plugin_version": "2.0.0",
|
|
55
|
-
"time_elapsed": 16752,
|
|
56
|
-
"attempted": false
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
"trial_index": 16,
|
|
60
|
-
"block_number": 1,
|
|
61
|
-
"image": "https://unyavwofnmwihnpvkjji.supabase.co/storage/v1/object/public/test-stimuli/gonogo/aliens/alien_trans_1.png",
|
|
62
|
-
"rt": 2000,
|
|
63
|
-
"stimulus": "",
|
|
64
|
-
"response": 0,
|
|
65
|
-
"trial_type": "html-button-response",
|
|
66
|
-
"plugin_version": "2.0.0",
|
|
67
|
-
"time_elapsed": 20290,
|
|
68
|
-
"attempted": false
|
|
69
|
-
},
|
|
70
|
-
// Non-gonogo data that should be filtered out
|
|
71
|
-
{ trial_type: 'instructions', text: 'Press space when...' },
|
|
72
|
-
{ trial_type: 'browser-check', browser: 'chrome' }
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
describe('gonogo_extract_trials', () => {
|
|
77
|
-
it('should extract trials from raw data', () => {
|
|
78
|
-
const trials = gonogo_extract_trials(gonogoData);
|
|
79
|
-
expect(trials.length).toBeLessThan(gonogoData.length);
|
|
80
|
-
|
|
81
|
-
trials.forEach(trial => {
|
|
82
|
-
expect(trial).toHaveProperty('stimulus');
|
|
83
|
-
expect(trial).toHaveProperty('stimulustype');
|
|
84
|
-
expect(trial).toHaveProperty('should_go');
|
|
85
|
-
expect(trial).toHaveProperty('did_go');
|
|
86
|
-
expect(trial).toHaveProperty('correct');
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('should handle empty data array', () => {
|
|
91
|
-
const trials = gonogo_extract_trials([]);
|
|
92
|
-
expect(trials).toEqual([]);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should extract mock data correctly', () => {
|
|
96
|
-
const trials = gonogo_extract_trials(mockGonogoData);
|
|
97
|
-
expect(trials.length).toBe(4); // Only trials with image field
|
|
98
|
-
|
|
99
|
-
// Check stimulus extraction and correctness logic
|
|
100
|
-
expect(trials[0].stimulus).toBe('astronaut_trans_1.png');
|
|
101
|
-
expect(trials[0].should_go).toBe(true); // astronaut in block 1
|
|
102
|
-
expect(trials[0].did_go).toBe(true); // response: 1
|
|
103
|
-
expect(trials[0].correct).toBe(true);
|
|
104
|
-
|
|
105
|
-
expect(trials[1].stimulus).toBe('alien_trans_1.png');
|
|
106
|
-
expect(trials[1].should_go).toBe(false); // alien in block 1
|
|
107
|
-
expect(trials[1].did_go).toBe(false); // response: null
|
|
108
|
-
expect(trials[1].correct).toBe(true);
|
|
109
|
-
|
|
110
|
-
expect(trials[2].stimulus).toBe('astronaut_trans_1.png');
|
|
111
|
-
expect(trials[2].should_go).toBe(true); // alien in block 1
|
|
112
|
-
expect(trials[2].did_go).toBe(false); // response: null
|
|
113
|
-
expect(trials[2].correct).toBe(false);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe('gonogo_results', () => {
|
|
118
|
-
it('should calculate correct averages', () => {
|
|
119
|
-
const sampleTrials = [
|
|
120
|
-
{ rt: 500, correct: true, should_go: true, did_go: true },
|
|
121
|
-
{ rt:
|
|
122
|
-
{ rt:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
expect(results.common.n_trials).toBe(
|
|
128
|
-
|
|
129
|
-
expect(results.common.
|
|
130
|
-
expect(results.common.
|
|
131
|
-
expect(results.common.performance_score).
|
|
132
|
-
expect(results).
|
|
133
|
-
expect(results
|
|
134
|
-
expect(results.special).toHaveProperty('
|
|
135
|
-
expect(results.special).toHaveProperty('
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
expect(mockResults.common.
|
|
150
|
-
expect(mockResults.common.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
expect(mockResults.common.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
expect(mockResults.common.performance_score).
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
expect(mockResults.special.
|
|
165
|
-
expect(mockResults.special.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
expect(results).toHaveProperty('
|
|
175
|
-
expect(results
|
|
176
|
-
expect(results.common).toHaveProperty('
|
|
177
|
-
expect(results.common).toHaveProperty('
|
|
178
|
-
expect(results.common).toHaveProperty('
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
expect(results.common.performance_score).
|
|
182
|
-
|
|
183
|
-
|
|
1
|
+
import { gonogo_extract_trials, gonogo_results, gonogo_calculate_results } from '../../gonogo';
|
|
2
|
+
import { loadTestData } from '../test-utils/testData';
|
|
3
|
+
|
|
4
|
+
describe('Gonogo test module', () => {
|
|
5
|
+
// If gonogo data is available in your test data, load it
|
|
6
|
+
// const gonogoData = loadTestData('gonogo').gonogo;
|
|
7
|
+
|
|
8
|
+
const gonogoData = loadTestData('gonogo').gonogo;
|
|
9
|
+
|
|
10
|
+
// Comprehensive mock data for predictable testing
|
|
11
|
+
const mockGonogoData = [
|
|
12
|
+
{
|
|
13
|
+
"trial_index": 8,
|
|
14
|
+
"block_number": 1,
|
|
15
|
+
"image": "https://unyavwofnmwihnpvkjji.supabase.co/storage/v1/object/public/test-stimuli/gonogo/aliens/astronaut_trans_1.png",
|
|
16
|
+
"rt": 1000,
|
|
17
|
+
"stimulus": "",
|
|
18
|
+
"response": 0,
|
|
19
|
+
"trial_type": "html-button-response",
|
|
20
|
+
"plugin_version": "2.0.0",
|
|
21
|
+
"time_elapsed": 15801,
|
|
22
|
+
"attempted": false
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"rt": null,
|
|
26
|
+
"stimulus": "<img src=\"https://unyavwofnmwihnpvkjji.supabase.co/storage/v1/object/public/test-stimuli/fixation_cross.png\" class=\"max-w-none\" style=\"display:none\" width=\"512\" height=\"512\" alt=\"Mapa\">",
|
|
27
|
+
"response": null,
|
|
28
|
+
"trial_type": "html-button-response",
|
|
29
|
+
"trial_index": 9,
|
|
30
|
+
"plugin_version": "2.0.0",
|
|
31
|
+
"time_elapsed": 16335,
|
|
32
|
+
"attempted": false
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"trial_index": 16,
|
|
36
|
+
"block_number": 1,
|
|
37
|
+
"image": "https://unyavwofnmwihnpvkjji.supabase.co/storage/v1/object/public/test-stimuli/gonogo/aliens/alien_trans_1.png",
|
|
38
|
+
"rt": null,
|
|
39
|
+
"stimulus": "",
|
|
40
|
+
"response": null,
|
|
41
|
+
"trial_type": "html-button-response",
|
|
42
|
+
"plugin_version": "2.0.0",
|
|
43
|
+
"time_elapsed": 20290,
|
|
44
|
+
"attempted": false
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"trial_index": 10,
|
|
48
|
+
"block_number": 1,
|
|
49
|
+
"image": "https://unyavwofnmwihnpvkjji.supabase.co/storage/v1/object/public/test-stimuli/gonogo/aliens/astronaut_trans_1.png",
|
|
50
|
+
"rt": null,
|
|
51
|
+
"stimulus": "",
|
|
52
|
+
"response": null,
|
|
53
|
+
"trial_type": "html-button-response",
|
|
54
|
+
"plugin_version": "2.0.0",
|
|
55
|
+
"time_elapsed": 16752,
|
|
56
|
+
"attempted": false
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"trial_index": 16,
|
|
60
|
+
"block_number": 1,
|
|
61
|
+
"image": "https://unyavwofnmwihnpvkjji.supabase.co/storage/v1/object/public/test-stimuli/gonogo/aliens/alien_trans_1.png",
|
|
62
|
+
"rt": 2000,
|
|
63
|
+
"stimulus": "",
|
|
64
|
+
"response": 0,
|
|
65
|
+
"trial_type": "html-button-response",
|
|
66
|
+
"plugin_version": "2.0.0",
|
|
67
|
+
"time_elapsed": 20290,
|
|
68
|
+
"attempted": false
|
|
69
|
+
},
|
|
70
|
+
// Non-gonogo data that should be filtered out
|
|
71
|
+
{ trial_type: 'instructions', text: 'Press space when...' },
|
|
72
|
+
{ trial_type: 'browser-check', browser: 'chrome' }
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
describe('gonogo_extract_trials', () => {
|
|
77
|
+
it('should extract trials from raw data', () => {
|
|
78
|
+
const trials = gonogo_extract_trials(gonogoData);
|
|
79
|
+
expect(trials.length).toBeLessThan(gonogoData.length);
|
|
80
|
+
|
|
81
|
+
trials.forEach(trial => {
|
|
82
|
+
expect(trial).toHaveProperty('stimulus');
|
|
83
|
+
expect(trial).toHaveProperty('stimulustype');
|
|
84
|
+
expect(trial).toHaveProperty('should_go');
|
|
85
|
+
expect(trial).toHaveProperty('did_go');
|
|
86
|
+
expect(trial).toHaveProperty('correct');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should handle empty data array', () => {
|
|
91
|
+
const trials = gonogo_extract_trials([]);
|
|
92
|
+
expect(trials).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should extract mock data correctly', () => {
|
|
96
|
+
const trials = gonogo_extract_trials(mockGonogoData);
|
|
97
|
+
expect(trials.length).toBe(4); // Only trials with image field
|
|
98
|
+
|
|
99
|
+
// Check stimulus extraction and correctness logic
|
|
100
|
+
expect(trials[0].stimulus).toBe('astronaut_trans_1.png');
|
|
101
|
+
expect(trials[0].should_go).toBe(true); // astronaut in block 1
|
|
102
|
+
expect(trials[0].did_go).toBe(true); // response: 1
|
|
103
|
+
expect(trials[0].correct).toBe(true);
|
|
104
|
+
|
|
105
|
+
expect(trials[1].stimulus).toBe('alien_trans_1.png');
|
|
106
|
+
expect(trials[1].should_go).toBe(false); // alien in block 1
|
|
107
|
+
expect(trials[1].did_go).toBe(false); // response: null
|
|
108
|
+
expect(trials[1].correct).toBe(true);
|
|
109
|
+
|
|
110
|
+
expect(trials[2].stimulus).toBe('astronaut_trans_1.png');
|
|
111
|
+
expect(trials[2].should_go).toBe(true); // alien in block 1
|
|
112
|
+
expect(trials[2].did_go).toBe(false); // response: null
|
|
113
|
+
expect(trials[2].correct).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('gonogo_results', () => {
|
|
118
|
+
it('should calculate correct averages', () => {
|
|
119
|
+
const sampleTrials = [
|
|
120
|
+
{ rt: 500, correct: true, should_go: true, did_go: true },
|
|
121
|
+
{ rt: null, correct: true, should_go: false, did_go: false },
|
|
122
|
+
{ rt: null, correct: false, should_go: true, did_go: false },
|
|
123
|
+
{ rt: 700, correct: true, should_go: true, did_go: true },
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const results = gonogo_results(sampleTrials);
|
|
127
|
+
expect(results.common.n_trials).toBe(4);
|
|
128
|
+
// Only trials with a response have an RT: (500 + 700) / 2
|
|
129
|
+
expect(results.common.avg_rt_total).toBe(600);
|
|
130
|
+
expect(results.common.n_correct_trials / results.common.n_trials).toBe(3/4);
|
|
131
|
+
expect(results.common.performance_score).toBeGreaterThan(0);
|
|
132
|
+
expect(results.common.performance_score).toBeLessThanOrEqual(100);
|
|
133
|
+
expect(results).toHaveProperty('special');
|
|
134
|
+
expect(results.special).toHaveProperty('go_trials');
|
|
135
|
+
expect(results.special).toHaveProperty('nogo_trials');
|
|
136
|
+
expect(results.special).toHaveProperty('by_block');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should handle empty trials array', () => {
|
|
140
|
+
expect(() => gonogo_results([])).toThrow();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should calculate mock data results correctly', () => {
|
|
144
|
+
const mockTrials = gonogo_extract_trials(mockGonogoData);
|
|
145
|
+
const mockResults = gonogo_results(mockTrials);
|
|
146
|
+
|
|
147
|
+
// Expected: 6 trials total, 6 correct, 0 incorrect, 2 missing
|
|
148
|
+
// (all trials are correct, but null responses still counted as missing)
|
|
149
|
+
expect(mockResults.common.n_trials).toBe(4);
|
|
150
|
+
expect(mockResults.common.n_correct_trials).toBe(2);
|
|
151
|
+
expect(mockResults.common.n_incorrect_trials).toBe(2);
|
|
152
|
+
|
|
153
|
+
expect(mockResults.common.avg_rt_total).toBe(1500);
|
|
154
|
+
|
|
155
|
+
// Expected avg_rt_correct: (500+600+400+700+800+0)/6 = 500 (all trials counted as correct)
|
|
156
|
+
expect(mockResults.common.avg_rt_correct).toBe(1000);
|
|
157
|
+
expect(mockResults.common.avg_rt_incorrect).toBe(2000);
|
|
158
|
+
|
|
159
|
+
// Performance score calculation using RT/(accuracy^5)
|
|
160
|
+
expect(mockResults.common.performance_score).toBeGreaterThanOrEqual(0);
|
|
161
|
+
expect(mockResults.common.performance_score).toBeLessThanOrEqual(100);
|
|
162
|
+
|
|
163
|
+
// Check special results
|
|
164
|
+
expect(mockResults.special.go_trials.n).toBe(2); // trials that should go
|
|
165
|
+
expect(mockResults.special.nogo_trials.n).toBe(2); // trials that should not go
|
|
166
|
+
expect(mockResults.special.by_block).toHaveLength(1); // 1 single block
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('gonogo_calculate_results', () => {
|
|
171
|
+
it('should process raw data and return valid results', () => {
|
|
172
|
+
const results = gonogo_calculate_results(gonogoData);
|
|
173
|
+
console.log(results);
|
|
174
|
+
expect(results).toHaveProperty('common');
|
|
175
|
+
expect(results).toHaveProperty('special');
|
|
176
|
+
expect(results.common).toHaveProperty('n_trials');
|
|
177
|
+
expect(results.common).toHaveProperty('n_correct_trials');
|
|
178
|
+
expect(results.common).toHaveProperty('avg_rt_total');
|
|
179
|
+
expect(results.common).toHaveProperty('performance_score');
|
|
180
|
+
|
|
181
|
+
expect(results.common.performance_score).toBeGreaterThan(0);
|
|
182
|
+
expect(results.common.performance_score).toBeLessThanOrEqual(100);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Full 4 x (50 go + 10 no-go) sessions built the way the runner builds them,
|
|
187
|
+
// with a response policy deciding each trial's RT (null = no click).
|
|
188
|
+
describe('simulated participants', () => {
|
|
189
|
+
const BASE = 'https://x.supabase.co/storage/v1/object/public/test-stimuli/gonogo/';
|
|
190
|
+
type Policy = (go: boolean, k: number) => number | null;
|
|
191
|
+
|
|
192
|
+
const session = (policy: Policy): any[] => {
|
|
193
|
+
const data: any[] = [{ trial_type: 'html-button-response', stimulus: 'instructions', response: 0, rt: 900 }];
|
|
194
|
+
[1, 2, 3, 4].forEach((block) => {
|
|
195
|
+
for (let i = 0; i < 60; i++) {
|
|
196
|
+
const go = i < 50;
|
|
197
|
+
const ufo = `ships/UFO_trans_${1 + (i % 10)}.png`;
|
|
198
|
+
const image = {
|
|
199
|
+
1: go ? 'aliens/astronaut_trans_1.png' : 'aliens/alien_trans_1.png',
|
|
200
|
+
2: go ? 'aliens/alien_trans_1.png' : 'aliens/astronaut_trans_1.png',
|
|
201
|
+
3: go ? 'ships/rocket_trans_1.png' : ufo,
|
|
202
|
+
4: go ? ufo : 'ships/rocket_trans_1.png',
|
|
203
|
+
}[block];
|
|
204
|
+
const rt = policy(go, (block - 1) * 60 + i);
|
|
205
|
+
data.push({ trial_type: 'html-button-response', block_number: block, image: BASE + image,
|
|
206
|
+
stimulus: '', response: rt === null ? null : 0, rt });
|
|
207
|
+
// fixation between trials
|
|
208
|
+
data.push({ trial_type: 'html-button-response', stimulus: '<img>', response: null, rt: null });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
return data;
|
|
212
|
+
};
|
|
213
|
+
const score = (policy: Policy) => gonogo_calculate_results(session(policy));
|
|
214
|
+
|
|
215
|
+
it('scores 0 for a participant who never responds', () => {
|
|
216
|
+
const r = score(() => null);
|
|
217
|
+
expect(r.common.performance_score).toBe(0);
|
|
218
|
+
expect(r.common.avg_rt_total).toBe(0);
|
|
219
|
+
expect(r.common.avg_rt_correct).toBe(0);
|
|
220
|
+
expect(r.common.avg_rt_incorrect).toBe(0);
|
|
221
|
+
expect(r.special.go_trials.n_omissions).toBe(200);
|
|
222
|
+
expect(r.special.balanced_acc).toBe(0.5);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('scores 0 for a participant who responds to everything', () => {
|
|
226
|
+
const r = score(() => 350);
|
|
227
|
+
expect(r.special.go_trials.avg_acc).toBe(1);
|
|
228
|
+
expect(r.special.nogo_trials.avg_acc).toBe(0);
|
|
229
|
+
expect(r.special.nogo_trials.n_commissions).toBe(40);
|
|
230
|
+
expect(r.special.balanced_acc).toBe(0.5);
|
|
231
|
+
expect(r.common.performance_score).toBe(0);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('gives a perfect participant a high score and the true RT everywhere', () => {
|
|
235
|
+
const r = score((go) => go ? 450 : null);
|
|
236
|
+
expect(r.common.performance_score).toBeCloseTo(96.8, 1);
|
|
237
|
+
expect(r.common.avg_rt_total).toBe(450);
|
|
238
|
+
expect(r.special.go_trials.avg_rt).toBe(450);
|
|
239
|
+
expect(r.special.nogo_trials.avg_rt).toBe(0);
|
|
240
|
+
expect(r.special.by_block).toHaveLength(4);
|
|
241
|
+
r.special.by_block.forEach((b: any) => {
|
|
242
|
+
expect(b.avg_rt).toBe(450); // correct no-go trials must not pull the mean down
|
|
243
|
+
expect(b.avg_acc).toBe(1);
|
|
244
|
+
expect(b.balanced_acc).toBe(1);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('does not count missed go trials as fast responses', () => {
|
|
249
|
+
const r = score((go, k) => go ? (k % 10 === 0 ? null : 500) : null);
|
|
250
|
+
expect(r.special.go_trials.avg_rt).toBe(500);
|
|
251
|
+
expect(r.special.go_trials.n_omissions).toBe(20);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('returns numbers, not NaN, when there are misses but no false alarms', () => {
|
|
255
|
+
const r = score((go, k) => go ? (k % 48 === 0 ? null : 450) : null);
|
|
256
|
+
expect(r.common.n_incorrect_trials).toBeGreaterThan(0);
|
|
257
|
+
expect(r.common.avg_rt_incorrect).toBe(0);
|
|
258
|
+
expect(Number.isNaN(r.common.avg_rt_correct)).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('ranks a careful participant above one who cannot withhold', () => {
|
|
262
|
+
// RT 600, misses 1 in 10 go trials, 1 false alarm in 10 no-go trials
|
|
263
|
+
const careful = score((go, k) => go ? (k % 10 === 0 ? null : 600) : (k % 60 === 50 ? 500 : null));
|
|
264
|
+
// RT 450, never misses, but clicks on half the no-go trials
|
|
265
|
+
const impulsive = score((go, k) => go ? 450 : (k % 2 ? 450 : null));
|
|
266
|
+
expect(careful.common.performance_score).toBeGreaterThan(impulsive.common.performance_score);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('does not modify the input data', () => {
|
|
270
|
+
const data = session((go) => go ? 450 : null);
|
|
271
|
+
const before = JSON.stringify(data);
|
|
272
|
+
gonogo_calculate_results(data);
|
|
273
|
+
expect(JSON.stringify(data)).toBe(before);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('prefers a should_go flag recorded by the runner', () => {
|
|
277
|
+
const trials = gonogo_extract_trials([
|
|
278
|
+
{ block_number: 1, image: BASE + 'aliens/alien_trans_1.png', should_go: true, response: 0, rt: 400 },
|
|
279
|
+
]);
|
|
280
|
+
expect(trials[0].should_go).toBe(true);
|
|
281
|
+
expect(trials[0].correct).toBe(true);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('drops trials whose image cannot be parsed instead of leaving them unscored', () => {
|
|
285
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
286
|
+
const trials = gonogo_extract_trials([
|
|
287
|
+
{ block_number: 1, image: BASE + 'aliens/astronaut_trans_1.png', response: 0, rt: 400 },
|
|
288
|
+
{ block_number: 1, image: 'broken.jpg', response: 0, rt: 400 },
|
|
289
|
+
]);
|
|
290
|
+
warn.mockRestore();
|
|
291
|
+
expect(trials).toHaveLength(1);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
184
294
|
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { nback_calculate_results, nback_group_results, inverse_normal } from '../../nback';
|
|
2
|
+
import { NbackResults } from '../../index';
|
|
3
|
+
import { loadTestData } from '../test-utils/testData';
|
|
4
|
+
|
|
5
|
+
// Builds version 2 trials as the runner saves them: n_back, block, block_index on every trial
|
|
6
|
+
const makeTrial = (block: string, block_index: number, n_back: number,
|
|
7
|
+
is_target: boolean, responded: boolean, rt = 600) => ({
|
|
8
|
+
trial_type: 'html-button-response',
|
|
9
|
+
is_target,
|
|
10
|
+
n_back,
|
|
11
|
+
block,
|
|
12
|
+
block_index,
|
|
13
|
+
test_version: '2.0',
|
|
14
|
+
response: responded ? 0 : null,
|
|
15
|
+
rt: responded ? rt : null,
|
|
16
|
+
correct: is_target === responded,
|
|
17
|
+
response_type: is_target
|
|
18
|
+
? (responded ? 'hit' : 'miss')
|
|
19
|
+
: (responded ? 'false_alarm' : 'correct_rejection'),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// One block: targets hits of n_targets, false_alarms of n_non_targets
|
|
23
|
+
const makeBlock = (block: string, block_index: number, n_back: number,
|
|
24
|
+
n_targets: number, hits: number, n_non_targets: number, false_alarms: number) => [
|
|
25
|
+
...Array.from({ length: n_targets }, (_, i) => makeTrial(block, block_index, n_back, true, i < hits)),
|
|
26
|
+
...Array.from({ length: n_non_targets }, (_, i) => makeTrial(block, block_index, n_back, false, i < false_alarms)),
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
describe('N-Back versioned results', () => {
|
|
30
|
+
beforeAll(() => { jest.spyOn(console, 'log').mockImplementation(() => {}); });
|
|
31
|
+
afterAll(() => { jest.restoreAllMocks(); });
|
|
32
|
+
|
|
33
|
+
describe('version 1 data', () => {
|
|
34
|
+
it('returns exactly the 0.3.0 result', () => {
|
|
35
|
+
const results = NbackResults(loadTestData('nback').nback);
|
|
36
|
+
// Output of the published compileprofile-spacetests@0.3.0 on the same data
|
|
37
|
+
expect(results).toEqual({
|
|
38
|
+
common: {
|
|
39
|
+
n_trials: 60,
|
|
40
|
+
n_correct_trials: 59,
|
|
41
|
+
n_incorrect_trials: 1,
|
|
42
|
+
avg_rt_total: 1066.2307692307693,
|
|
43
|
+
avg_rt_correct: 1065.0833333333333,
|
|
44
|
+
avg_rt_incorrect: 1080,
|
|
45
|
+
performance_score: 98.33333333333333,
|
|
46
|
+
},
|
|
47
|
+
special: {
|
|
48
|
+
target_trials: { n: 12, avg_rt: 1065.0833333333333, avg_acc: 100 },
|
|
49
|
+
non_target_trials: { n: 48, avg_rt: 22.5, avg_acc: 97.91666666666667 },
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('has no by_block or by_nback', () => {
|
|
55
|
+
const results = NbackResults(loadTestData('nback').nback);
|
|
56
|
+
expect(results.special.by_block).toBeUndefined();
|
|
57
|
+
expect(results.special.by_nback).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('version 2 data', () => {
|
|
62
|
+
const data = [
|
|
63
|
+
{ trial_type: 'data-trial', data_type: 'test-settings', test_version: '2.0' },
|
|
64
|
+
{ trial_type: 'browser-check', browser: 'chrome' },
|
|
65
|
+
...makeBlock('abstract', 0, 2, 3, 3, 12, 0),
|
|
66
|
+
...makeBlock('fractals', 1, 2, 3, 2, 12, 1),
|
|
67
|
+
...makeBlock('3d', 2, 3, 3, 1, 12, 2),
|
|
68
|
+
...makeBlock('planets', 3, 4, 3, 0, 12, 0),
|
|
69
|
+
];
|
|
70
|
+
const results = nback_calculate_results(data);
|
|
71
|
+
|
|
72
|
+
it('keeps common over all trials', () => {
|
|
73
|
+
expect(results.common.n_trials).toBe(60);
|
|
74
|
+
expect(results.common.n_correct_trials).toBe(60 - 6 - 3);
|
|
75
|
+
expect(results.special.target_trials.n).toBe(12);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('splits results by block in block order', () => {
|
|
79
|
+
const byBlock = results.special.by_block!;
|
|
80
|
+
expect(byBlock.map(b => [b.block, b.block_index, b.n_back]))
|
|
81
|
+
.toEqual([['abstract', 0, 2], ['fractals', 1, 2], ['3d', 2, 3], ['planets', 3, 4]]);
|
|
82
|
+
expect(byBlock.map(b => b.hits)).toEqual([3, 2, 1, 0]);
|
|
83
|
+
expect(byBlock.map(b => b.false_alarms)).toEqual([0, 1, 2, 0]);
|
|
84
|
+
expect(byBlock.every(b => b.n_trials === 15)).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('pools blocks with the same N', () => {
|
|
88
|
+
const byNback = results.special.by_nback!;
|
|
89
|
+
expect(byNback.map(l => l.n_back)).toEqual([2, 3, 4]);
|
|
90
|
+
const two = byNback[0];
|
|
91
|
+
expect(two.n_trials).toBe(30);
|
|
92
|
+
expect(two.n_targets).toBe(6);
|
|
93
|
+
expect(two.hits).toBe(5);
|
|
94
|
+
expect(two.misses).toBe(1);
|
|
95
|
+
expect(two.false_alarms).toBe(1);
|
|
96
|
+
expect(two.correct_rejections).toBe(23);
|
|
97
|
+
expect(two.hit_rate).toBeCloseTo(5 / 6);
|
|
98
|
+
expect(two.false_alarm_rate).toBeCloseTo(1 / 24);
|
|
99
|
+
expect(two.performance_score).toBeCloseTo(28 * 100 / 30);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('orders N levels numerically regardless of block order', () => {
|
|
103
|
+
const reordered = nback_calculate_results([
|
|
104
|
+
...makeBlock('a', 0, 4, 3, 1, 12, 0),
|
|
105
|
+
...makeBlock('b', 1, 2, 3, 1, 12, 0),
|
|
106
|
+
...makeBlock('c', 2, 10, 3, 1, 12, 0),
|
|
107
|
+
]);
|
|
108
|
+
expect(reordered.special.by_nback!.map(l => l.n_back)).toEqual([2, 4, 10]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('averages reaction times of hits and false alarms only', () => {
|
|
112
|
+
const group = nback_group_results([
|
|
113
|
+
makeTrial('a', 0, 2, true, true, 400),
|
|
114
|
+
makeTrial('a', 0, 2, true, true, 800),
|
|
115
|
+
makeTrial('a', 0, 2, true, false),
|
|
116
|
+
makeTrial('a', 0, 2, false, true, 1000),
|
|
117
|
+
makeTrial('a', 0, 2, false, false),
|
|
118
|
+
]);
|
|
119
|
+
expect(group.avg_rt_hits).toBe(600);
|
|
120
|
+
expect(group.avg_rt_false_alarms).toBe(1000);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('lures', () => {
|
|
125
|
+
const lure = (responded: boolean) => ({ ...makeTrial('a', 0, 3, false, responded), is_lure: true });
|
|
126
|
+
|
|
127
|
+
it('counts lures and their false alarms separately', () => {
|
|
128
|
+
const group = nback_group_results([
|
|
129
|
+
...makeBlock('a', 0, 3, 4, 3, 9, 1), // 1 false alarm on a fresh image
|
|
130
|
+
lure(true), lure(true), lure(false),
|
|
131
|
+
]);
|
|
132
|
+
expect(group.n_lures).toBe(3);
|
|
133
|
+
expect(group.lure_false_alarms).toBe(2);
|
|
134
|
+
expect(group.lure_false_alarm_rate).toBeCloseTo(2 / 3);
|
|
135
|
+
// lures are non-targets: their false alarms count in the overall false alarms too
|
|
136
|
+
expect(group.n_non_targets).toBe(12);
|
|
137
|
+
expect(group.false_alarms).toBe(3);
|
|
138
|
+
expect(group.false_alarm_rate).toBeCloseTo(3 / 12);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('is null without lures', () => {
|
|
142
|
+
const group = nback_group_results(makeBlock('a', 0, 2, 4, 4, 12, 0));
|
|
143
|
+
expect(group.n_lures).toBe(0);
|
|
144
|
+
expect(group.lure_false_alarm_rate).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('appears per block and per N', () => {
|
|
148
|
+
const results = nback_calculate_results([
|
|
149
|
+
...makeBlock('a', 0, 2, 4, 4, 10, 0), { ...makeTrial('a', 0, 2, false, true), is_lure: true },
|
|
150
|
+
...makeBlock('b', 1, 3, 4, 4, 10, 0), { ...makeTrial('b', 1, 3, false, false), is_lure: true },
|
|
151
|
+
]);
|
|
152
|
+
expect(results.special.by_block!.map(b => [b.n_lures, b.lure_false_alarms])).toEqual([[1, 1], [1, 0]]);
|
|
153
|
+
expect(results.special.by_nback!.map(l => l.lure_false_alarm_rate)).toEqual([1, 0]);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("d'", () => {
|
|
158
|
+
it('matches the standard normal quantiles', () => {
|
|
159
|
+
expect(inverse_normal(0.5)).toBeCloseTo(0, 8);
|
|
160
|
+
expect(inverse_normal(0.975)).toBeCloseTo(1.959964, 5);
|
|
161
|
+
expect(inverse_normal(0.01)).toBeCloseTo(-2.326348, 5);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('uses the log-linear correction and stays finite at perfect performance', () => {
|
|
165
|
+
const perfect = nback_group_results(makeBlock('a', 0, 2, 3, 3, 12, 0));
|
|
166
|
+
// z((3 + .5) / 4) - z((0 + .5) / 13)
|
|
167
|
+
expect(perfect.d_prime).toBeCloseTo(inverse_normal(0.875) - inverse_normal(0.5 / 13), 8);
|
|
168
|
+
expect(Number.isFinite(perfect.d_prime)).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('is around 0 when responding at random', () => {
|
|
172
|
+
const chance = nback_group_results(makeBlock('a', 0, 2, 10, 5, 10, 5));
|
|
173
|
+
expect(chance.d_prime).toBeCloseTo(0, 8);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('is null without targets or without non-targets', () => {
|
|
177
|
+
expect(nback_group_results(makeBlock('a', 0, 2, 0, 0, 10, 2)).d_prime).toBeNull();
|
|
178
|
+
expect(nback_group_results(makeBlock('a', 0, 2, 3, 2, 0, 0)).d_prime).toBeNull();
|
|
179
|
+
expect(nback_group_results(makeBlock('a', 0, 2, 0, 0, 10, 2)).hit_rate).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|