neurotask 0.2.0__tar.gz

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 (44) hide show
  1. neurotask-0.2.0/LICENSE +21 -0
  2. neurotask-0.2.0/PKG-INFO +240 -0
  3. neurotask-0.2.0/README.md +221 -0
  4. neurotask-0.2.0/neurotask/neurotask/__init__.py +0 -0
  5. neurotask-0.2.0/neurotask/neurotask/tmt/__init__.py +0 -0
  6. neurotask-0.2.0/neurotask/neurotask/tmt/config.py +8 -0
  7. neurotask-0.2.0/neurotask/neurotask/tmt/crosses/__init__.py +0 -0
  8. neurotask-0.2.0/neurotask/neurotask/tmt/crosses/crosses.py +131 -0
  9. neurotask-0.2.0/neurotask/neurotask/tmt/crosses/crosses_metric_calculator.py +20 -0
  10. neurotask-0.2.0/neurotask/neurotask/tmt/cut_criteria/__init__.py +0 -0
  11. neurotask-0.2.0/neurotask/neurotask/tmt/cut_criteria/cut_criteria.py +6 -0
  12. neurotask-0.2.0/neurotask/neurotask/tmt/cut_criteria/cut_implementation.py +146 -0
  13. neurotask-0.2.0/neurotask/neurotask/tmt/invalid_cause.py +17 -0
  14. neurotask-0.2.0/neurotask/neurotask/tmt/mapper/__init__.py +0 -0
  15. neurotask-0.2.0/neurotask/neurotask/tmt/mapper/mapper.py +21 -0
  16. neurotask-0.2.0/neurotask/neurotask/tmt/metrics/__init__.py +0 -0
  17. neurotask-0.2.0/neurotask/neurotask/tmt/metrics/area_calculation.py +82 -0
  18. neurotask-0.2.0/neurotask/neurotask/tmt/metrics/base_metric.py +37 -0
  19. neurotask-0.2.0/neurotask/neurotask/tmt/metrics/difference_from_ideal_distance.py +38 -0
  20. neurotask-0.2.0/neurotask/neurotask/tmt/metrics/distance_calculation.py +61 -0
  21. neurotask-0.2.0/neurotask/neurotask/tmt/metrics/intra_and_inter_target_time.py +148 -0
  22. neurotask-0.2.0/neurotask/neurotask/tmt/metrics/metrics_calculator.py +417 -0
  23. neurotask-0.2.0/neurotask/neurotask/tmt/metrics/sampling_period.py +89 -0
  24. neurotask-0.2.0/neurotask/neurotask/tmt/metrics/speed_metrics.py +228 -0
  25. neurotask-0.2.0/neurotask/neurotask/tmt/metrics/targets_touched.py +253 -0
  26. neurotask-0.2.0/neurotask/neurotask/tmt/metrics/zig_zag_amplitud.py +52 -0
  27. neurotask-0.2.0/neurotask/neurotask/tmt/model/__init__.py +0 -0
  28. neurotask-0.2.0/neurotask/neurotask/tmt/model/tmt_model.py +165 -0
  29. neurotask-0.2.0/neurotask/neurotask/tmt/preprocessing/__init__.py +0 -0
  30. neurotask-0.2.0/neurotask/neurotask/tmt/preprocessing/interpolation.py +62 -0
  31. neurotask-0.2.0/neurotask/neurotask/tmt/segmentation/__init__.py +0 -0
  32. neurotask-0.2.0/neurotask/neurotask/tmt/segmentation/segmentation.py +539 -0
  33. neurotask-0.2.0/neurotask/neurotask/tmt/segmentation/segmentation_metric.py +27 -0
  34. neurotask-0.2.0/neurotask/neurotask/tmt/tmt_analyzer.py +188 -0
  35. neurotask-0.2.0/neurotask/neurotask/tmt/visualization/__init__.py +0 -0
  36. neurotask-0.2.0/neurotask/neurotask/tmt/visualization/utils.py +63 -0
  37. neurotask-0.2.0/neurotask/neurotask/tmt/visualization/visualization.py +345 -0
  38. neurotask-0.2.0/neurotask/neurotask.egg-info/PKG-INFO +240 -0
  39. neurotask-0.2.0/neurotask/neurotask.egg-info/SOURCES.txt +42 -0
  40. neurotask-0.2.0/neurotask/neurotask.egg-info/dependency_links.txt +1 -0
  41. neurotask-0.2.0/neurotask/neurotask.egg-info/requires.txt +6 -0
  42. neurotask-0.2.0/neurotask/neurotask.egg-info/top_level.txt +1 -0
  43. neurotask-0.2.0/pyproject.toml +35 -0
  44. neurotask-0.2.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Gianluca Capelo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,240 @@
1
+ Metadata-Version: 2.4
2
+ Name: neurotask
3
+ Version: 0.2.0
4
+ Summary: A Python package for analyzing neuropsychological tasks data
5
+ Author: NeuroLIAA
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/NeuroLIAA/neurotask
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: numpy>=1.24
14
+ Requires-Dist: pandas>=2.0
15
+ Requires-Dist: matplotlib>=3.7
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ # Neurotask
21
+
22
+ ## Description
23
+
24
+ **Neurotask** is a Python library designed to facilitate the analysis and processing of neuropsychological task data.
25
+
26
+ ## Installation
27
+
28
+ You can install **neurotask** via pip:
29
+
30
+ ```bash
31
+ pip install neurotask
32
+ ```
33
+
34
+ or from the local repository (run from the repository root, where `pyproject.toml` is):
35
+
36
+ ```bash
37
+ pip install .
38
+ ```
39
+
40
+ If you're developing the package and want changes to the code to be immediately reflected without reinstalling, you can
41
+ use the -e option to install the package in "editable" mode, including the development dependencies:
42
+
43
+ ```bash
44
+ pip install -e ".[dev]"
45
+ ```
46
+
47
+ ## Usage Example
48
+
49
+ Here's a simple example of how to use the **neurotask** library to perform a Trail Making Test (TMT) analysis:
50
+
51
+ ```python
52
+ from neurotask.tmt.tmt_analyzer import TMTAnalyzer
53
+ from / path / to / your / custom_mapper
54
+ import CustomMapper
55
+
56
+ # Initialize the TMT Analyzer
57
+ analysis = TMTAnalyzer(
58
+ mapper=CustomMapper(),
59
+ dataset_path="/path/to/your/dataset",
60
+ output_path="/path/to/save/results",
61
+ correct_targets_minimum=12,
62
+ consecutive_points=5
63
+ )
64
+
65
+ # Run the analysis
66
+ analysis.run()
67
+
68
+ # Retrieve and display metrics
69
+ metrics_df = analysis.get_metrics_dataframe()
70
+ print(metrics_df.head())
71
+
72
+ # Retrive the experiment to access tmt model
73
+ experiment_model = analysis.experiment
74
+ ```
75
+
76
+ ## CSV Output Format
77
+
78
+ After running the TMT analysis, the results are saved as a CSV file. Each row in the CSV represents a trial (valid or
79
+ invalid) and contains the following columns:
80
+
81
+ | Column Name | Data Type | Description |
82
+ |------------------------------|----------------|----------------------------------------------------------------------------------------------|
83
+ | **subject_id** | String | Unique identifier for the subject. |
84
+ | **trial_id** | String | Unique identifier for the trial. |
85
+ | **trial_type** | String | The type of trial, represented by its name. |
86
+ | **age** | Number | Age of the subject (in years). |
87
+ | **gender** | String | Gender of the subject. |
88
+ | **is_valid** | Boolean | Indicates whether the trial is valid (`True`) or invalid (`False`). |
89
+ | **trial_order_of_appearance** | Integer | The sequential order of the trial within the experiment. |
90
+ | **speed_threshold** | Float | The speed threshold calculated for the subject. |
91
+ | **total_distance** | Float | Total distance traveled by the cursor during the trial. |
92
+ | **rt** | Float | Reaction time (total duration) of the trial. |
93
+ | **inter_target_time** | Float | Total time outside targets during the trial. |
94
+ | **intra_target_time** | Float | Total time inside targets during the trial. |
95
+ | **correct_targets_touches** | Integer or NaN | Number of correct target touches. For invalid trials, this is set to `NaN`. |
96
+ | **wrong_targets_touches** | Integer or NaN | Number of wrong target touches. For invalid trials, this is set to `NaN`. |
97
+ | **mean_speed** | Float or NaN | Average cursor speed during the trial. |
98
+ | **std_speed** | Float or NaN | Standard deviation of the cursor speed. |
99
+ | **peak_speed** | Float or NaN | Maximum cursor speed observed during the trial. |
100
+ | **mean_acceleration** | Float or NaN | Average acceleration of the cursor during the trial. |
101
+ | **std_acceleration** | Float or NaN | Standard deviation of the cursor acceleration. |
102
+ | **peak_acceleration** | Float or NaN | Maximum acceleration observed during the trial. |
103
+ | **mean_abs_acceleration** | Float or NaN | Average of the absolute acceleration values. |
104
+ | **std_abs_acceleration** | Float or NaN | Standard deviation of the absolute acceleration values. |
105
+ | **peak_abs_acceleration** | Float or NaN | Maximum absolute acceleration observed. |
106
+ | **mean_negative_acceleration** | Float or NaN | Average of the negative acceleration values (deceleration). |
107
+ | **std_negative_acceleration** | Float or NaN | Standard deviation of the negative acceleration values. |
108
+ | **peak_negative_acceleration** | Float or NaN | Most negative acceleration value observed. |
109
+ | **hesitation_distance** | Float or NaN | Distance over which hesitation was observed (if applicable). |
110
+ | **hesitation_time** | Float or NaN | Duration of hesitation (if applicable). |
111
+ | **number_of_crosses** | Integer or NaN | Number of crosses detected in the trial. Calculated only if enabled; otherwise, set to `NaN`. |
112
+ | **invalid_cause** | String | For invalid trials, a description of why the trial was marked invalid. |
113
+
114
+ > **Note:** For invalid trials, many of the metric columns (e.g., distances, speeds, accelerations) will be set to
115
+ > default values or `NaN` to indicate that the trial did not pass validation checks.
116
+
117
+
118
+ ## Detailed Segment Data
119
+
120
+ In addition to the aggregated metrics CSV, **neurotask** provides access to detailed segment data for each trial. This data includes information about the segments where correct and incorrect target touches occurred. The detailed segment data is exposed via the `get_segments_data()` method in the `TMTAnalyzer` class. This method returns a dictionary where each key is a `subject_id` and each value is a list of trial segment data for that subject.
121
+
122
+ For each trial, the segment data includes:
123
+
124
+ - **trial_id**: Unique identifier for the trial.
125
+ - **correct_segments**: A list of dictionaries, each representing a segment where a correct target was touched.
126
+ - **incorrect_segments**: A list of dictionaries, each representing a segment where an incorrect target was touched.
127
+
128
+ ### Segment Structure
129
+
130
+ Each segment dictionary (in both `correct_segments` and `incorrect_segments`) includes the following keys:
131
+
132
+ - **target**: Dictionary with details of the target, including:
133
+ - **content**: *String* — The content or label of the target.
134
+ - **position**: *Dictionary* — The position of the target.
135
+ - **start_cursor**: Dictionary with details about the cursor when it first entered the target area, including:
136
+ - **position**: *Dictionary* — The starting cursor position.
137
+ - **time**: *Float* — The timestamp when the cursor entered the target area.
138
+ - **end_cursor**: Dictionary with details about the cursor when it exited the target area, including:
139
+ - **position**: *Dictionary* — The ending cursor position.
140
+ - **time**: *Float* — The timestamp when the cursor exited the target area.
141
+
142
+ **Position Dictionary Structure**:
143
+ For any dictionary that includes a `position` key, the structure is as follows:
144
+ - **x**: *Float* — The x-coordinate.
145
+ - **y**: *Float* — The y-coordinate.
146
+ ### Usage Example
147
+
148
+ Below is an example of how to retrieve and work with the detailed segment data:
149
+
150
+ ```python
151
+ from neurotask.tmt.tmt_analyzer import TMTAnalyzer
152
+ from my_mapper import CustomMapper
153
+
154
+ # Initialize the TMT Analyzer
155
+ analyzer = TMTAnalyzer(
156
+ mapper=CustomMapper(),
157
+ dataset_path="path/to/your/dataset",
158
+ output_path="path/to/save/results"
159
+ )
160
+
161
+ # Run the analysis (override parameters as needed)
162
+ analyzer.run(
163
+ correct_targets_minimum=12,
164
+ consecutive_points=5,
165
+ cut_criteria="MINIMUM_TARGETS",
166
+ calculate_crosses=True
167
+ )
168
+
169
+ # Retrieve the detailed segments data
170
+ segments_data = analyzer.get_segments_data()
171
+
172
+ # Example: Access data for a specific subject
173
+ subject_id = "subject_1"
174
+ if subject_id in segments_data:
175
+ for trial_data in segments_data[subject_id]:
176
+ print(f"Trial ID: {trial_data['trial_id']}")
177
+ print("Correct segments:", trial_data["correct_segments"])
178
+ print("Incorrect segments:", trial_data["incorrect_segments"])
179
+ else:
180
+ print(f"No segment data found for {subject_id}")
181
+ ```
182
+
183
+ ### Example: Detailed Segment Data for a Single Trial
184
+
185
+ Below is a sample JSON representation of the segment data for one trial of a given subject. In this example, the subject participated in trial `"trial_id_3"`. This example contains only one correct segment and one incorrect segment.
186
+
187
+ ```json
188
+ {
189
+ "trial_id": "trial_id_3",
190
+ "correct_segments": [
191
+ {
192
+ "target": {
193
+ "content": "A",
194
+ "position": {
195
+ "x": 123.45,
196
+ "y": 67.89
197
+ }
198
+ },
199
+ "start_cursor": {
200
+ "position": {
201
+ "x": 120.00,
202
+ "y": 65.00
203
+ },
204
+ "time": 0.75
205
+ },
206
+ "end_cursor": {
207
+ "position": {
208
+ "x": 123.45,
209
+ "y": 67.89
210
+ },
211
+ "time": 1.25
212
+ }
213
+ }
214
+ ],
215
+ "incorrect_segments": [
216
+ {
217
+ "target": {
218
+ "content": "B",
219
+ "position": {
220
+ "x": 200.00,
221
+ "y": 150.00
222
+ }
223
+ },
224
+ "start_cursor": {
225
+ "position": {
226
+ "x": 195.00,
227
+ "y": 145.00
228
+ },
229
+ "time": 2.50
230
+ },
231
+ "end_cursor": {
232
+ "position": {
233
+ "x": 200.00,
234
+ "y": 150.00
235
+ },
236
+ "time": 3.00
237
+ }
238
+ }
239
+ ]
240
+ }
@@ -0,0 +1,221 @@
1
+ # Neurotask
2
+
3
+ ## Description
4
+
5
+ **Neurotask** is a Python library designed to facilitate the analysis and processing of neuropsychological task data.
6
+
7
+ ## Installation
8
+
9
+ You can install **neurotask** via pip:
10
+
11
+ ```bash
12
+ pip install neurotask
13
+ ```
14
+
15
+ or from the local repository (run from the repository root, where `pyproject.toml` is):
16
+
17
+ ```bash
18
+ pip install .
19
+ ```
20
+
21
+ If you're developing the package and want changes to the code to be immediately reflected without reinstalling, you can
22
+ use the -e option to install the package in "editable" mode, including the development dependencies:
23
+
24
+ ```bash
25
+ pip install -e ".[dev]"
26
+ ```
27
+
28
+ ## Usage Example
29
+
30
+ Here's a simple example of how to use the **neurotask** library to perform a Trail Making Test (TMT) analysis:
31
+
32
+ ```python
33
+ from neurotask.tmt.tmt_analyzer import TMTAnalyzer
34
+ from / path / to / your / custom_mapper
35
+ import CustomMapper
36
+
37
+ # Initialize the TMT Analyzer
38
+ analysis = TMTAnalyzer(
39
+ mapper=CustomMapper(),
40
+ dataset_path="/path/to/your/dataset",
41
+ output_path="/path/to/save/results",
42
+ correct_targets_minimum=12,
43
+ consecutive_points=5
44
+ )
45
+
46
+ # Run the analysis
47
+ analysis.run()
48
+
49
+ # Retrieve and display metrics
50
+ metrics_df = analysis.get_metrics_dataframe()
51
+ print(metrics_df.head())
52
+
53
+ # Retrive the experiment to access tmt model
54
+ experiment_model = analysis.experiment
55
+ ```
56
+
57
+ ## CSV Output Format
58
+
59
+ After running the TMT analysis, the results are saved as a CSV file. Each row in the CSV represents a trial (valid or
60
+ invalid) and contains the following columns:
61
+
62
+ | Column Name | Data Type | Description |
63
+ |------------------------------|----------------|----------------------------------------------------------------------------------------------|
64
+ | **subject_id** | String | Unique identifier for the subject. |
65
+ | **trial_id** | String | Unique identifier for the trial. |
66
+ | **trial_type** | String | The type of trial, represented by its name. |
67
+ | **age** | Number | Age of the subject (in years). |
68
+ | **gender** | String | Gender of the subject. |
69
+ | **is_valid** | Boolean | Indicates whether the trial is valid (`True`) or invalid (`False`). |
70
+ | **trial_order_of_appearance** | Integer | The sequential order of the trial within the experiment. |
71
+ | **speed_threshold** | Float | The speed threshold calculated for the subject. |
72
+ | **total_distance** | Float | Total distance traveled by the cursor during the trial. |
73
+ | **rt** | Float | Reaction time (total duration) of the trial. |
74
+ | **inter_target_time** | Float | Total time outside targets during the trial. |
75
+ | **intra_target_time** | Float | Total time inside targets during the trial. |
76
+ | **correct_targets_touches** | Integer or NaN | Number of correct target touches. For invalid trials, this is set to `NaN`. |
77
+ | **wrong_targets_touches** | Integer or NaN | Number of wrong target touches. For invalid trials, this is set to `NaN`. |
78
+ | **mean_speed** | Float or NaN | Average cursor speed during the trial. |
79
+ | **std_speed** | Float or NaN | Standard deviation of the cursor speed. |
80
+ | **peak_speed** | Float or NaN | Maximum cursor speed observed during the trial. |
81
+ | **mean_acceleration** | Float or NaN | Average acceleration of the cursor during the trial. |
82
+ | **std_acceleration** | Float or NaN | Standard deviation of the cursor acceleration. |
83
+ | **peak_acceleration** | Float or NaN | Maximum acceleration observed during the trial. |
84
+ | **mean_abs_acceleration** | Float or NaN | Average of the absolute acceleration values. |
85
+ | **std_abs_acceleration** | Float or NaN | Standard deviation of the absolute acceleration values. |
86
+ | **peak_abs_acceleration** | Float or NaN | Maximum absolute acceleration observed. |
87
+ | **mean_negative_acceleration** | Float or NaN | Average of the negative acceleration values (deceleration). |
88
+ | **std_negative_acceleration** | Float or NaN | Standard deviation of the negative acceleration values. |
89
+ | **peak_negative_acceleration** | Float or NaN | Most negative acceleration value observed. |
90
+ | **hesitation_distance** | Float or NaN | Distance over which hesitation was observed (if applicable). |
91
+ | **hesitation_time** | Float or NaN | Duration of hesitation (if applicable). |
92
+ | **number_of_crosses** | Integer or NaN | Number of crosses detected in the trial. Calculated only if enabled; otherwise, set to `NaN`. |
93
+ | **invalid_cause** | String | For invalid trials, a description of why the trial was marked invalid. |
94
+
95
+ > **Note:** For invalid trials, many of the metric columns (e.g., distances, speeds, accelerations) will be set to
96
+ > default values or `NaN` to indicate that the trial did not pass validation checks.
97
+
98
+
99
+ ## Detailed Segment Data
100
+
101
+ In addition to the aggregated metrics CSV, **neurotask** provides access to detailed segment data for each trial. This data includes information about the segments where correct and incorrect target touches occurred. The detailed segment data is exposed via the `get_segments_data()` method in the `TMTAnalyzer` class. This method returns a dictionary where each key is a `subject_id` and each value is a list of trial segment data for that subject.
102
+
103
+ For each trial, the segment data includes:
104
+
105
+ - **trial_id**: Unique identifier for the trial.
106
+ - **correct_segments**: A list of dictionaries, each representing a segment where a correct target was touched.
107
+ - **incorrect_segments**: A list of dictionaries, each representing a segment where an incorrect target was touched.
108
+
109
+ ### Segment Structure
110
+
111
+ Each segment dictionary (in both `correct_segments` and `incorrect_segments`) includes the following keys:
112
+
113
+ - **target**: Dictionary with details of the target, including:
114
+ - **content**: *String* — The content or label of the target.
115
+ - **position**: *Dictionary* — The position of the target.
116
+ - **start_cursor**: Dictionary with details about the cursor when it first entered the target area, including:
117
+ - **position**: *Dictionary* — The starting cursor position.
118
+ - **time**: *Float* — The timestamp when the cursor entered the target area.
119
+ - **end_cursor**: Dictionary with details about the cursor when it exited the target area, including:
120
+ - **position**: *Dictionary* — The ending cursor position.
121
+ - **time**: *Float* — The timestamp when the cursor exited the target area.
122
+
123
+ **Position Dictionary Structure**:
124
+ For any dictionary that includes a `position` key, the structure is as follows:
125
+ - **x**: *Float* — The x-coordinate.
126
+ - **y**: *Float* — The y-coordinate.
127
+ ### Usage Example
128
+
129
+ Below is an example of how to retrieve and work with the detailed segment data:
130
+
131
+ ```python
132
+ from neurotask.tmt.tmt_analyzer import TMTAnalyzer
133
+ from my_mapper import CustomMapper
134
+
135
+ # Initialize the TMT Analyzer
136
+ analyzer = TMTAnalyzer(
137
+ mapper=CustomMapper(),
138
+ dataset_path="path/to/your/dataset",
139
+ output_path="path/to/save/results"
140
+ )
141
+
142
+ # Run the analysis (override parameters as needed)
143
+ analyzer.run(
144
+ correct_targets_minimum=12,
145
+ consecutive_points=5,
146
+ cut_criteria="MINIMUM_TARGETS",
147
+ calculate_crosses=True
148
+ )
149
+
150
+ # Retrieve the detailed segments data
151
+ segments_data = analyzer.get_segments_data()
152
+
153
+ # Example: Access data for a specific subject
154
+ subject_id = "subject_1"
155
+ if subject_id in segments_data:
156
+ for trial_data in segments_data[subject_id]:
157
+ print(f"Trial ID: {trial_data['trial_id']}")
158
+ print("Correct segments:", trial_data["correct_segments"])
159
+ print("Incorrect segments:", trial_data["incorrect_segments"])
160
+ else:
161
+ print(f"No segment data found for {subject_id}")
162
+ ```
163
+
164
+ ### Example: Detailed Segment Data for a Single Trial
165
+
166
+ Below is a sample JSON representation of the segment data for one trial of a given subject. In this example, the subject participated in trial `"trial_id_3"`. This example contains only one correct segment and one incorrect segment.
167
+
168
+ ```json
169
+ {
170
+ "trial_id": "trial_id_3",
171
+ "correct_segments": [
172
+ {
173
+ "target": {
174
+ "content": "A",
175
+ "position": {
176
+ "x": 123.45,
177
+ "y": 67.89
178
+ }
179
+ },
180
+ "start_cursor": {
181
+ "position": {
182
+ "x": 120.00,
183
+ "y": 65.00
184
+ },
185
+ "time": 0.75
186
+ },
187
+ "end_cursor": {
188
+ "position": {
189
+ "x": 123.45,
190
+ "y": 67.89
191
+ },
192
+ "time": 1.25
193
+ }
194
+ }
195
+ ],
196
+ "incorrect_segments": [
197
+ {
198
+ "target": {
199
+ "content": "B",
200
+ "position": {
201
+ "x": 200.00,
202
+ "y": 150.00
203
+ }
204
+ },
205
+ "start_cursor": {
206
+ "position": {
207
+ "x": 195.00,
208
+ "y": 145.00
209
+ },
210
+ "time": 2.50
211
+ },
212
+ "end_cursor": {
213
+ "position": {
214
+ "x": 200.00,
215
+ "y": 150.00
216
+ },
217
+ "time": 3.00
218
+ }
219
+ }
220
+ ]
221
+ }
File without changes
File without changes
@@ -0,0 +1,8 @@
1
+ """
2
+ Centralized configuration for TMT analysis.
3
+ """
4
+
5
+ # Maximum valid speed threshold (in pixels/millisecond).
6
+ # Speeds above this value are considered measurement errors.
7
+ INVALID_SPEED_THRESHOLD = 8.0
8
+
@@ -0,0 +1,131 @@
1
+ from typing import List, Tuple
2
+
3
+ from ..model.tmt_model import TMTTrial, Coordinate
4
+
5
+ def calculate_crosses_for_trial(
6
+ trial: TMTTrial,
7
+ crosses_time_threshold: float,
8
+ ) -> Tuple[int, List[Tuple[Tuple[Coordinate, Coordinate], Tuple[Coordinate, Coordinate], float]]]:
9
+ """
10
+ Calculates the number of times the cursor trail crosses itself, excluding segments that are very near in time.
11
+
12
+ Parameters:
13
+ - trial: TMTTrial object containing the cursor_trail.
14
+ - crosses_time_threshold: float, the minimum time gap (in milliseconds) between segments to consider them for intersection.
15
+ Segments with gap < threshold are excluded as "too close in time".
16
+
17
+ Returns:
18
+ - num_crosses: int, the number of times the cursor trail crosses itself.
19
+ - cross_segments: list of tuples: (seg1, seg2, time_gap) for each detected crossing.
20
+ time_gap is in milliseconds.
21
+ """
22
+ cursor_trail = trial.get_cursor_trail_from_start()
23
+
24
+ # Not enough points to form non-adjacent segments
25
+ if len(cursor_trail) < 4:
26
+ return 0, []
27
+
28
+ # Build segments with endpoint times
29
+ segments: List[Tuple[Tuple[Coordinate, float], Tuple[Coordinate, float]]] = []
30
+ for i in range(len(cursor_trail) - 1):
31
+ p1 = cursor_trail[i].position
32
+ p2 = cursor_trail[i + 1].position
33
+ t1 = cursor_trail[i].time
34
+ t2 = cursor_trail[i + 1].time
35
+ segments.append(((p1, t1), (p2, t2)))
36
+
37
+ num_crosses = 0
38
+ cross_segments: List[Tuple[Tuple[Coordinate, Coordinate], Tuple[Coordinate, Coordinate], float]] = []
39
+
40
+ for i in range(len(segments)):
41
+ for j in range(i + 1, len(segments)):
42
+ # Skip adjacent segments (they share a point)
43
+ if j == i + 1:
44
+ continue
45
+
46
+ # Time interval for each segment
47
+ t_start_i = min(segments[i][0][1], segments[i][1][1])
48
+ t_end_i = max(segments[i][0][1], segments[i][1][1])
49
+ t_start_j = min(segments[j][0][1], segments[j][1][1])
50
+ t_end_j = max(segments[j][0][1], segments[j][1][1])
51
+
52
+ # Compute gap between time intervals (0 if overlapping)
53
+ if t_end_i < t_start_j:
54
+ time_gap = t_start_j - t_end_i
55
+ elif t_end_j < t_start_i:
56
+ time_gap = t_start_i - t_end_j
57
+ else:
58
+ time_gap = 0.0
59
+
60
+ # Skip if segments are too close in time
61
+ if time_gap < crosses_time_threshold:
62
+ continue
63
+
64
+ seg1 = (segments[i][0][0], segments[i][1][0])
65
+ seg2 = (segments[j][0][0], segments[j][1][0])
66
+
67
+ if segments_intersect(seg1[0], seg1[1], seg2[0], seg2[1]):
68
+ num_crosses += 1
69
+ cross_segments.append((seg1, seg2, time_gap))
70
+
71
+ return num_crosses, cross_segments
72
+
73
+
74
+
75
+ def orientation(p: Coordinate, q: Coordinate, r: Coordinate) -> int:
76
+ """
77
+ Determines the orientation of the triplet (p, q, r).
78
+
79
+ Returns:
80
+ - 0 : Colinear
81
+ - 1 : Clockwise
82
+ - 2 : Counterclockwise
83
+ """
84
+ val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y)
85
+ if abs(val) < 1e-10:
86
+ return 0 # Colinear
87
+ elif val > 0:
88
+ return 1 # Clockwise
89
+ else:
90
+ return 2 # Counterclockwise
91
+
92
+
93
+ def on_segment(p: Coordinate, q: Coordinate, r: Coordinate) -> bool:
94
+ """
95
+ Checks if point q lies on segment pr.
96
+ """
97
+ if min(p.x, r.x) <= q.x <= max(p.x, r.x) and \
98
+ min(p.y, r.y) <= q.y <= max(p.y, r.y):
99
+ return True
100
+ return False
101
+
102
+
103
+ def segments_intersect(p1: Coordinate, q1: Coordinate, p2: Coordinate, q2: Coordinate) -> bool:
104
+ """
105
+ Checks if two line segments (p1,q1) and (p2,q2) intersect.
106
+ """
107
+ # Find the four orientations needed for general and special cases
108
+ o1 = orientation(p1, q1, p2)
109
+ o2 = orientation(p1, q1, q2)
110
+ o3 = orientation(p2, q2, p1)
111
+ o4 = orientation(p2, q2, q1)
112
+
113
+ # General case
114
+ if o1 != o2 and o3 != o4:
115
+ return True
116
+
117
+ # Special Cases
118
+ # p1, q1 and p2 are colinear and p2 lies on segment p1q1
119
+ if o1 == 0 and on_segment(p1, p2, q1):
120
+ return True
121
+ # p1, q1 and q2 are colinear and q2 lies on segment p1q1
122
+ if o2 == 0 and on_segment(p1, q2, q1):
123
+ return True
124
+ # p2, q2 and p1 are colinear and p1 lies on segment p2q2
125
+ if o3 == 0 and on_segment(p2, p1, q2):
126
+ return True
127
+ # p2, q2 and q1 are colinear and q1 lies on segment p2q2
128
+ if o4 == 0 and on_segment(p2, q1, q2):
129
+ return True
130
+
131
+ return False
@@ -0,0 +1,20 @@
1
+ import numpy as np
2
+ from neurotask.tmt.crosses.crosses import calculate_crosses_for_trial
3
+ from neurotask.tmt.metrics.base_metric import BaseMetricCalculator
4
+ from neurotask.tmt.model.tmt_model import TMTTrial, TMTSubject, TMTTarget, CursorInfo
5
+
6
+
7
+ class CrossesMetricCalculator(BaseMetricCalculator):
8
+ def add_metrics(self, metrics: dict, trial: TMTTrial, subject: TMTSubject,
9
+ trails_between_targets: list[tuple[TMTTarget, list[CursorInfo]]], calculate_crosses: bool,
10
+ speed_threshold, consecutive_points, target_radius_multiplier: float,
11
+ crosses_time_threshold: float) -> dict:
12
+ metric_key = self.get_metric_name("number_of_crosses")
13
+
14
+ if calculate_crosses:
15
+ num_crosses, _ = calculate_crosses_for_trial(trial, crosses_time_threshold)
16
+ metrics[metric_key] = num_crosses
17
+ else:
18
+ metrics[metric_key] = np.nan
19
+
20
+ return metrics
@@ -0,0 +1,6 @@
1
+ from enum import Enum
2
+
3
+
4
+ class CutCriteria(Enum):
5
+
6
+ MINIMUM_TARGETS = "MINIMUM_TARGETS"