MultiTaskBattery 1.3.0__py3-none-any.whl

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.
@@ -0,0 +1,23 @@
1
+ name task_class descriptive_name code
2
+ rest Rest rest rest
3
+ motor_localizer MotorLocalizer motor localizer motloc
4
+ toe_movement FlexionExtension toe flexexten
5
+ finger_sequence FingerSequence finger_sequence fingseq
6
+ serial_reaction_time SerialReactionTime serial_reaction_time srt
7
+ tongue_movement TongueMovement tongue tongue
8
+ finger_rhythmic FingerRhythmic finger_rhythmic fingrhytm
9
+ time_perception TimePerception time_perception timeperc
10
+ visual_search VisualSearch search visrchsml
11
+ action_observation ActionObservation knot actobserv
12
+ n_back NBack memory nbko
13
+ demand_grid DemandGrid grid dmgrd
14
+ oddball OddBall oddball odbal
15
+ passage_listening PassageListening listen pssglisten
16
+ reading Reading read read
17
+ semantic_prediction SemanticPrediction complete sempred
18
+ verb_generation VerbGeneration verb vrbgen
19
+ movie Movie movie mov
20
+ auditory_narrative AuditoryNarrative radio audnarr
21
+ spatial_navigation SpatialNavigation imagine spatialnav
22
+ affective Affective affective affect
23
+ theory_of_mind TheoryOfMind story tom
@@ -0,0 +1,73 @@
1
+ # Created 2023: Bassel Arafat, Jorn Diedrichsen, Ince Husain
2
+ from psychopy import core, event
3
+
4
+ class TTLClock:
5
+
6
+ def __init__(self):
7
+ """
8
+ TTLClock class is used for counting the number of TTL pulses and the time of the last TTL pulse
9
+ """
10
+ self.clock = core.Clock() #
11
+ self.ttl_count = 0 # the number of ttl pulses
12
+ self.ttl_time = 0 # time stamp of the last incoming ttl pulse
13
+ self.ttl_button = 't' # the button used for simulating a ttl pulse
14
+
15
+ def reset(self, start_time=0):
16
+ """ resets the clock and ttl-counter
17
+ Args:
18
+ start_time (float):
19
+ the time in seconds to start the clock from
20
+ """
21
+ self.clock.reset(start_time)
22
+ self.ttl_count = 0
23
+ self.ttl_time = 0
24
+
25
+ def wait_for_first_ttl(self, wait = True):
26
+ """ This function waits for the first TTL and then resets the clock appropriately
27
+ Args:
28
+ wait (bool):
29
+ if True, the function will wait for the first TTL
30
+ """
31
+ if wait:
32
+ print('Waiting for first TTL...')
33
+ while self.ttl_count == 0:
34
+ self.update()
35
+ self.clock.reset()
36
+ self.ttl_time = 0
37
+
38
+ def wait_until(self,time):
39
+ """waits until the given time since the beginning of the run
40
+ Args:
41
+ time (float):
42
+ time in seconds
43
+ Returns:
44
+ real_time (float):
45
+ time that the real start occurs
46
+ start_ttl (int):
47
+ Number of TRs recorded since the beginning of the run
48
+ start_ttl_time (float):
49
+ Time since the last TR """
50
+ while self.clock.getTime() < time:
51
+ self.update()
52
+ return self.clock.getTime(), self.ttl_count, self.clock.getTime()-self.ttl_time
53
+
54
+ def get_time(self):
55
+ """returns the current time of the clock
56
+ Returns:
57
+ time (float):
58
+ the current time of the clock
59
+ """
60
+ return self.clock.getTime()
61
+
62
+ def update(self):
63
+ """ updates the ttl count and time of the last ttl pulse
64
+ """
65
+ # get all the ttl pulses in the buffer (ttl_button is one key or a list of keys)
66
+ ttl_keys = [self.ttl_button] if isinstance(self.ttl_button, str) else list(self.ttl_button)
67
+ keys = event.getKeys(ttl_keys, timeStamped=self.clock)
68
+
69
+ # checks if the pressed key is the key used as the ttl pulse
70
+ for k in keys:
71
+ self.ttl_count += 1 # each time a ttl button is pressed, ttl count increases
72
+ self.ttl_time = k[1] # the time when the ttl button has been pressed
73
+ print(f"TR count: {self.ttl_count} - TR time: {self.ttl_time} (key '{k[0]}')", end = "\r")
@@ -0,0 +1,201 @@
1
+ # Created 2023: Bassel Arafat, Jorn Diedrichsen, Ince Hussain
2
+ import os
3
+ from pathlib import Path
4
+ import pandas as pd
5
+ import MultiTaskBattery.task_blocks as tasks
6
+ import MultiTaskBattery.task_file as task_files
7
+ from MultiTaskBattery.fetch import CACHE_DIR
8
+
9
+ # DEPRECATED: kept for backwards compatibility with external user scripts.
10
+ # make_files.py no longer consults this list — instead it inspects each task's
11
+ # make_task_file signature for a run_number parameter. To opt your task out of
12
+ # receiving a run_number, simply omit it from the signature.
13
+ tasks_without_run_number = ['n_back', 'verb_generation', 'rest', 'tongue_movement',
14
+ 'oddball', 'demand_grid', 'demand_grid_easy_diff','finger_sequence', 'finger_sequence_surprise', 'flexion_extension',
15
+ 'visual_search', 'serial_reaction_time', 'rest_surprise', 'rest_surprise_images', 'rest_surprise_sound_images','temp_deviant']
16
+
17
+ def dircheck(path2dir):
18
+ """
19
+ Checks if a directory exists! if it does not exist, it creates it
20
+ Args:
21
+ dir_path (str, path)
22
+ path to the directory you want to be created
23
+ """
24
+ if not os.path.exists(path2dir):
25
+ print(f"creating {path2dir}")
26
+ os.makedirs(path2dir)
27
+
28
+ def append_data_to_file(filename,data):
29
+ """ Appends a data frame to an (possibly) existing tsv file
30
+ Args:
31
+ filename (str):
32
+ path to the file
33
+ data (dataframe):
34
+ data to be appended to the file
35
+ """
36
+ if os.path.isfile(filename):
37
+ old_data = pd.read_csv(filename, sep = '\t')
38
+ data = pd.concat([old_data,data],axis = 0)
39
+ data.to_csv(filename, sep = '\t', index = False)
40
+
41
+ def module_dirs(task_modules):
42
+ """Folders holding the task modules: a task library keeps its
43
+ task_table.tsv and stimuli/ next to its code."""
44
+ return [Path(m.__file__).resolve().parent for m in (task_modules or [])]
45
+
46
+ def get_task_table(exp_dir=None, task_modules=None):
47
+ """ Reads the task_table.tsv files and concatenates them, avoiding duplicates.
48
+ Tables are read in order: next to each task module, then the experiment's,
49
+ then the package's; for a name in more than one table, the first one wins.
50
+ Args:
51
+ exp_dir (str, path, optional):
52
+ path to the experiment directory. If None, its table is not read.
53
+ task_modules (list, optional):
54
+ task modules (const.task_modules); a task_table.tsv next to a
55
+ module is read, e.g. from a task library outside the experiment.
56
+ Returns:
57
+ task_table (dataframe):
58
+ dataframe containing the task table
59
+ """
60
+ tables = [d / 'task_table.tsv' for d in module_dirs(task_modules)]
61
+ if exp_dir is not None:
62
+ tables.append(Path(exp_dir) / 'task_table.tsv')
63
+ tables.append(Path(__file__).parent / 'task_table.tsv') # package table last, as fallback
64
+ task_table = pd.concat([pd.read_csv(t, sep='\t') for t in tables if os.path.isfile(t)], axis=0)
65
+ return task_table.drop_duplicates(subset='name').reset_index(drop=True)
66
+
67
+ def get_task_class(const, class_name):
68
+ """ Searches for the task class in the list of task modules and returns it
69
+ Args:
70
+ const (constant object):
71
+ constant.py object containing the list of task modules to search for the task class
72
+ class_name (str):
73
+ name of the task class to be searched for
74
+ Returns:
75
+ TaskClass (class):
76
+ the task class that was searched for
77
+ """
78
+ # First tries to find the class in the custom list of task modules
79
+ if hasattr(const, 'task_modules'):
80
+ for module in const.task_modules:
81
+ if hasattr(module, class_name):
82
+ return getattr(module, class_name)
83
+ if hasattr(tasks, class_name):
84
+ return getattr(tasks, class_name)
85
+ else:
86
+ raise NameError(f"Task class {class_name} not found in any of the task modules, make sure to add the module to the list of task_modules in constants.py")
87
+
88
+ def get_task_file_class(const, class_name):
89
+ """ Searches for the TaskFile class in the list of task modules and returns it.
90
+ Mirrors get_task_class but for the file-generation side. Custom TaskFile
91
+ classes follow a '<class>File' naming convention so they can coexist with
92
+ their matching Task class in the same module (e.g. SilentWord and
93
+ SilentWordFile in my_tasks.py).
94
+
95
+ Args:
96
+ const (constant object):
97
+ constants.py object containing the task_modules list
98
+ class_name (str):
99
+ base class name from task_table.tsv (e.g. 'SilentWord')
100
+ Returns:
101
+ TaskFileClass (class):
102
+ the TaskFile class that was searched for
103
+ """
104
+ # First tries '<class>File' in the custom list of task modules
105
+ if hasattr(const, 'task_modules'):
106
+ suffixed = class_name + 'File'
107
+ for module in const.task_modules:
108
+ if hasattr(module, suffixed):
109
+ return getattr(module, suffixed)
110
+ # Otherwise fall back to the framework's task_file module (bare class name)
111
+ if hasattr(task_files, class_name):
112
+ return getattr(task_files, class_name)
113
+ else:
114
+ raise NameError(f"TaskFile class {class_name} (or {class_name}File) not found in any task module, make sure to add the module to task_modules in constants.py")
115
+
116
+
117
+ # --- Stimulus resolution -------------------------------------------------
118
+ # Stimuli are resolved with the same local-first, package-fallback logic that
119
+ # task *classes* use (see get_task_class / get_task_file_class): each root is
120
+ # searched in order, and the package's own bundled stimuli are always appended
121
+ # as a final fallback. This lets a custom task keep its stimuli in the
122
+ # experiment folder while built-in tasks still resolve from the package.
123
+ # The package's stimuli live at <repo_root>/stimuli, i.e. one level up from this
124
+ # module's package directory. After those comes the stimulus cache that
125
+ # `mtb fetch` downloads into (see fetch.py).
126
+ _PACKAGE_STIM_DIR = Path(__file__).resolve().parents[1] / 'stimuli'
127
+
128
+
129
+ def stim_roots(const):
130
+ """Return the ordered list of stimulus root directories to search.
131
+
132
+ Searches the ``stimuli`` folder next to each module in ``const.task_modules``
133
+ first, then ``const.stim_dirs`` (a list of roots) if it is defined, otherwise
134
+ the single ``const.stim_dir``. The package's bundled ``stimuli`` folder is
135
+ appended next, then the stimulus cache filled by ``mtb fetch``
136
+ (deduplicated), so built-in tasks still resolve.
137
+
138
+ Args:
139
+ const: the experiment constants module/object.
140
+ Returns:
141
+ list[Path]: roots to search, in priority order (local first, package last).
142
+ """
143
+ roots = [d / 'stimuli' for d in module_dirs(getattr(const, 'task_modules', None))]
144
+ roots += getattr(const, 'stim_dirs', None) or [getattr(const, 'stim_dir', _PACKAGE_STIM_DIR)]
145
+ roots.append(_PACKAGE_STIM_DIR)
146
+ roots.append(CACHE_DIR)
147
+ return list(dict.fromkeys(Path(r).resolve() for r in roots))
148
+
149
+
150
+ def find_stim(const, task_name, *parts, must_exist=True):
151
+ """Resolve a single stimulus file for a task across the stimulus roots.
152
+
153
+ Searches ``<root>/<task_name>/<parts...>`` in each root (local first, package
154
+ last) and returns the first path that exists. Because the local roots are
155
+ searched first, appending the package fallback can never change a lookup that
156
+ already succeeds - it can only rescue one that would otherwise fail.
157
+
158
+ Args:
159
+ const: the experiment constants module/object.
160
+ task_name (str): task subfolder name (usually ``self.name``).
161
+ *parts (str): path components under the task folder - a filename, or e.g.
162
+ ``'clips', 'foo.mov'`` for a nested subfolder.
163
+ must_exist (bool): if True (default) raise ``FileNotFoundError`` when the
164
+ file is in none of the roots; if False, return the path under the
165
+ first root (useful when writing a file rather than reading one).
166
+ Returns:
167
+ Path: the resolved stimulus path.
168
+ """
169
+ roots = stim_roots(const)
170
+ for r in roots:
171
+ p = r.joinpath(task_name, *parts)
172
+ if p.exists():
173
+ return p
174
+ if must_exist:
175
+ sub = str(Path(*parts)) if parts else ''
176
+ raise FileNotFoundError(
177
+ f"Stimulus '{task_name}/{sub}' not found in any stimulus root: "
178
+ f"{[str(r) for r in roots]}. If it is a built-in task, run `mtb fetch {task_name}`.")
179
+ return roots[0].joinpath(task_name, *parts)
180
+
181
+
182
+ def find_stim_dir(const, task_name):
183
+ """Resolve a task's stimulus *directory* across the stimulus roots.
184
+
185
+ Returns the first ``<root>/<task_name>`` that is an existing directory. Use
186
+ this for the cases that operate on the folder itself (e.g. globbing for a set
187
+ of files) rather than resolving one named file. If no root has the folder,
188
+ the path under the first root is returned so callers get a sensible location
189
+ for error messages.
190
+
191
+ Args:
192
+ const: the experiment constants module/object.
193
+ task_name (str): task subfolder name (usually ``self.name``).
194
+ Returns:
195
+ Path: the resolved task stimulus directory.
196
+ """
197
+ for r in stim_roots(const):
198
+ d = r / task_name
199
+ if d.is_dir():
200
+ return d
201
+ return stim_roots(const)[0] / task_name
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: MultiTaskBattery
3
+ Version: 1.3.0
4
+ Summary: A flexible Python toolbox for running multi-domain cognitive and motor tasks during fMRI sessions, built on PsychoPy.
5
+ Author: Diedrichsenlab
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/diedrichsenlab/MultiTaskBattery
8
+ Project-URL: Documentation, https://multitaskbattery.readthedocs.io/
9
+ Project-URL: Issues, https://github.com/diedrichsenlab/MultiTaskBattery/issues
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Scientific/Engineering
14
+ Requires-Python: <3.13,>=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: psychopy==2026.2.5
18
+ Requires-Dist: pandas
19
+ Requires-Dist: numpy>=1.26.4
20
+ Requires-Dist: scipy
21
+ Requires-Dist: moviepy
22
+ Requires-Dist: imageio
23
+ Requires-Dist: ffmpeg-python
24
+ Requires-Dist: sounddevice
25
+ Requires-Dist: soundfile
26
+ Requires-Dist: pyyaml
27
+ Requires-Dist: questplus
28
+ Requires-Dist: pooch
29
+ Requires-Dist: numba
30
+ Requires-Dist: nibabel
31
+ Requires-Dist: psychopy-sounddevice
32
+ Provides-Extra: docs
33
+ Requires-Dist: numpy; extra == "docs"
34
+ Requires-Dist: sphinx>=1.4; extra == "docs"
35
+ Requires-Dist: sphinxcontrib-bibtex; extra == "docs"
36
+ Requires-Dist: sphinx-copybutton; extra == "docs"
37
+ Requires-Dist: pandas; extra == "docs"
38
+ Requires-Dist: matplotlib; extra == "docs"
39
+ Requires-Dist: ipykernel; extra == "docs"
40
+ Requires-Dist: nbsphinx; extra == "docs"
41
+ Requires-Dist: scipy; extra == "docs"
42
+ Requires-Dist: seaborn; extra == "docs"
43
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
44
+ Requires-Dist: sphinx-design; extra == "docs"
45
+ Dynamic: license-file
46
+
47
+ <p align="center">
48
+ <img src="https://raw.githubusercontent.com/DiedrichsenLab/MultiTaskBattery/main/docs/assets/icon_MTB.png" alt="MTB Logo" width="200">
49
+ </p>
50
+
51
+ # Multi Task Battery (MTB)
52
+
53
+ A flexible Python toolbox for running multi-domain cognitive and motor tasks during fMRI sessions — all within a single scanning run. Built on [PsychoPy](https://www.psychopy.org/)
54
+
55
+ [![Documentation](https://img.shields.io/badge/docs-readthedocs-blue)](https://multitaskbattery.readthedocs.io/)
56
+ [![Interactive task map](https://img.shields.io/badge/explore-interactive%20task%20map-orange)](https://diedrichsenlab.github.io/MultiTaskBattery/mds/)
57
+
58
+
59
+ This project runs multi-task batteries developed by the diedrichsenlab.
60
+ #### Authors: Diedrichsenlab (Bassel Arafat, Caroline Nettekoven, Ince Husain, Ladan Shahshahani, Suzanne Witt, Maedbh King, Jorn Diedrichsen)
61
+
62
+
63
+ ## Documentation
64
+
65
+ Full documentation is hosted at:
66
+ https://multitaskbattery.readthedocs.io/
67
+
68
+ Covers:
69
+ - Installation and setup
70
+ - Task and run configuration
71
+ - Optimal battery selection
72
+ - Data output
73
+
74
+ ---
75
+
76
+ ## License
77
+
78
+ MIT License © 2024 Joern Diedrichsen
79
+
80
+ ---
81
+
82
+ ## Contributing
83
+
84
+ Open to issues and pull requests. If you use MTB in your research.
85
+
86
+ ---
87
+
88
+ ## Citation
89
+
90
+ If you use the toolbox please cite the following reference:
91
+
92
+ Arafat, B., Nettekoven, C., Xiang, J. D., & Diedrichsen, J. (2026). Multi-Task Batteries for Precision Functional Mapping. *eLife*, 15, RP111868. https://doi.org/10.7554/eLife.111868.1
@@ -0,0 +1,17 @@
1
+ MultiTaskBattery/__init__.py,sha256=7jB7NL8qDIauzVTg1zOzIiQSsE5TqfZ-zqqMZTrbpg0,149
2
+ MultiTaskBattery/battery.py,sha256=4s4Q1DVA7UbRmezgZgOGRh735KgRTodNWUjmRXQbUJQ,8070
3
+ MultiTaskBattery/experiment_block.py,sha256=4m8NolckTj0wpa1jIVcq57StjSLyLMu-EGPGaeTKv_M,15945
4
+ MultiTaskBattery/fetch.py,sha256=Q-4TS9XOHOu-RHrhichkTC1ED90ZK8-aBmMhBbwrRfM,3021
5
+ MultiTaskBattery/screen.py,sha256=Inf5XR_yJAw1NXBclZ_bwi7SESB04_JFMRlfslgvpDc,5760
6
+ MultiTaskBattery/task_blocks.py,sha256=mQibCR2WqEWs8dVIZivcGyc3-oBm0RwKmnhXJ3t10lk,74441
7
+ MultiTaskBattery/task_details.json,sha256=UxX2ZwrH8O6FmbDR-j5oD1pft16fWN0W_2rcAbfyVEE,59150
8
+ MultiTaskBattery/task_file.py,sha256=gSRzX0VDjPlpB8JEWDsjXCIKieKDr-6EoZ5O7CkkAuc,77438
9
+ MultiTaskBattery/task_table.tsv,sha256=HWyMpUkQinfYSXeBj2XWY-qcm1WQ5Y182FxhjdpN9dI,1024
10
+ MultiTaskBattery/ttl_clock.py,sha256=j-gS9RTec8RvsszP1eqFp-7P20hSsKdx_tpZCdNqXWg,2787
11
+ MultiTaskBattery/utils.py,sha256=SGmYP6OtRNYSm3SyspiGAT4hggKdYGN83cvD2v8amS8,9285
12
+ multitaskbattery-1.3.0.dist-info/licenses/LICENSE,sha256=jJJvF_95za41k5Fhb5e1O4E8cRdmFcL4FT0G6-Ll8NY,1074
13
+ multitaskbattery-1.3.0.dist-info/METADATA,sha256=TK6n7aykg3r6zohq1nDShqU9kQ_loHETJW0pXJW_rHA,3149
14
+ multitaskbattery-1.3.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
15
+ multitaskbattery-1.3.0.dist-info/entry_points.txt,sha256=jNF1OcHTqnbjILkUMoP9kenlcThpsf71IfNAErfJU4M,52
16
+ multitaskbattery-1.3.0.dist-info/top_level.txt,sha256=X9uviKkeqfHhUbRcbTsvr2bPR2HLhdLVX2HHFfoo8X4,17
17
+ multitaskbattery-1.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mtb = MultiTaskBattery.fetch:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Joern Diedrichsen
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 @@
1
+ MultiTaskBattery