oceanproc 0.1.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 (40) hide show
  1. oceanproc-0.1.0/PKG-INFO +29 -0
  2. oceanproc-0.1.0/README.md +2 -0
  3. oceanproc-0.1.0/oceanproc/__init__.py +1 -0
  4. oceanproc-0.1.0/oceanproc/bids_wrapper.py +331 -0
  5. oceanproc-0.1.0/oceanproc/events_long.py +153 -0
  6. oceanproc-0.1.0/oceanproc/firstlevel/__init__.py +0 -0
  7. oceanproc-0.1.0/oceanproc/firstlevel/run_glm.py +923 -0
  8. oceanproc-0.1.0/oceanproc/firstlevel/tests/__init__.py +0 -0
  9. oceanproc-0.1.0/oceanproc/firstlevel/tests/test_run_glm.py +165 -0
  10. oceanproc-0.1.0/oceanproc/fmriprep_wrapper.py +222 -0
  11. oceanproc-0.1.0/oceanproc/group_series.py +228 -0
  12. oceanproc-0.1.0/oceanproc/grouplevel/__init__.py +0 -0
  13. oceanproc-0.1.0/oceanproc/grouplevel/computations.py +80 -0
  14. oceanproc-0.1.0/oceanproc/grouplevel/design_mat_utils.py +54 -0
  15. oceanproc-0.1.0/oceanproc/grouplevel/dscalar_utils.py +51 -0
  16. oceanproc-0.1.0/oceanproc/grouplevel/run.py +18 -0
  17. oceanproc-0.1.0/oceanproc/grouplevel/utils.py +4 -0
  18. oceanproc-0.1.0/oceanproc/ocean_proc.py +215 -0
  19. oceanproc-0.1.0/oceanproc/oceanparse.py +107 -0
  20. oceanproc-0.1.0/oceanproc/utils.py +224 -0
  21. oceanproc-0.1.0/oceanproc/web/TODO.md +4 -0
  22. oceanproc-0.1.0/oceanproc/web/__init__.py +0 -0
  23. oceanproc-0.1.0/oceanproc/web/app.py +78 -0
  24. oceanproc-0.1.0/oceanproc/web/config.json +7 -0
  25. oceanproc-0.1.0/oceanproc/web/get_parser_json.py +58 -0
  26. oceanproc-0.1.0/oceanproc/web/static/components/ArgparseForm.js +9 -0
  27. oceanproc-0.1.0/oceanproc/web/static/components/ArgparseFormEntry.js +52 -0
  28. oceanproc-0.1.0/oceanproc/web/static/components/DirIcon.js +11 -0
  29. oceanproc-0.1.0/oceanproc/web/static/components/FileBrowser.js +85 -0
  30. oceanproc-0.1.0/oceanproc/web/static/components/FileBrowserOpenBtn.js +12 -0
  31. oceanproc-0.1.0/oceanproc/web/static/components/FileEntry.js +32 -0
  32. oceanproc-0.1.0/oceanproc/web/static/components/FileIcon.js +11 -0
  33. oceanproc-0.1.0/oceanproc/web/static/components/OceanprocForm.js +53 -0
  34. oceanproc-0.1.0/oceanproc/web/static/components/styles.css +44 -0
  35. oceanproc-0.1.0/oceanproc/web/templates/filesystem.html +13 -0
  36. oceanproc-0.1.0/oceanproc/web/templates/filesystem_fail.html +2 -0
  37. oceanproc-0.1.0/oceanproc/web/templates/oceanproc_form.html +23 -0
  38. oceanproc-0.1.0/oceanproc/web/templates/react-test.html +0 -0
  39. oceanproc-0.1.0/oceanproc/web/templates/username_card_component_test.html +39 -0
  40. oceanproc-0.1.0/pyproject.toml +36 -0
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.1
2
+ Name: oceanproc
3
+ Version: 0.1.0
4
+ Summary: MRI preprocessing for WUSTL Ocean labs
5
+ Author: Ramone Agard
6
+ Author-email: agardr@wustl.edu
7
+ Requires-Python: >=3.10,<3.13
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Dist: asttokens (>=2.4.1,<3.0.0)
13
+ Requires-Dist: beautifulsoup4 (>=4.12.0)
14
+ Requires-Dist: flask (>=3.0.3,<4.0.0)
15
+ Requires-Dist: matplotlib (>=3.9.1,<4.0.0)
16
+ Requires-Dist: nilearn (>=0.10.4,<0.11.0)
17
+ Requires-Dist: numba (>=0.60.0,<0.61.0)
18
+ Requires-Dist: numpy (>=1.26.0)
19
+ Requires-Dist: pandas (>=2.2.2,<3.0.0)
20
+ Requires-Dist: scipy (>=1.13.0)
21
+ Requires-Dist: sphinx-rtd-theme (>=2.0.0,<3.0.0)
22
+ Requires-Dist: torch (>=2.5.1,<3.0.0)
23
+ Requires-Dist: torchaudio (>=2.5.1,<3.0.0)
24
+ Requires-Dist: torchvision (>=0.20.1,<0.21.0)
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Ocean-Proc
28
+ MRI preprocessing for WUSTL Ocean labs
29
+
@@ -0,0 +1,2 @@
1
+ # Ocean-Proc
2
+ MRI preprocessing for WUSTL Ocean labs
@@ -0,0 +1 @@
1
+ __all__ = ["bids_wrapper", "events_long", "group_series", "ocean_proc", "oceanparse"]
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from argparse import ArgumentParser
4
+ from collections import OrderedDict
5
+ from glob import glob
6
+ from pathlib import Path
7
+ import json
8
+ import os
9
+ import re
10
+ import shlex
11
+ import shutil
12
+ import subprocess
13
+ from textwrap import dedent
14
+ import xml.etree.ElementTree as et
15
+ from .utils import exit_program_early, prompt_user_continue, prepare_subprocess_logging, debug_logging, log_linebreak, flags
16
+ import logging
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ @debug_logging
21
+ def remove_unusable_runs(xml_file:Path, bids_data_path:Path, subject:str):
22
+ """
23
+ Will remove unusable scans from list of scans after dcm2bids has run.
24
+
25
+ :param xml_file: Path to XML file containing quality information generated by XNAT.
26
+ :type xml_file: pathlib.Path
27
+ :param bids_data_path: Path to directory containing BIDS-compliant data for the given subject.
28
+ :type bids_data_path: pathlib.Path
29
+ :param subject: Subject ID used in BIDS-compliant data (for example, if 'sub-5000', subject is '5000').
30
+ :type subject: str
31
+
32
+ """
33
+ log_linebreak()
34
+ logger.info("####### Removing the scans marked 'unusable' #######\n")
35
+
36
+ if not xml_file.exists():
37
+ exit_program_early(f"Path {str(xml_file)} does not exist.")
38
+
39
+ tree = et.parse(xml_file)
40
+ prefix = "{" + str(tree.getroot()).split("{")[-1].split("}")[0] + "}"
41
+ scan_element_list = list(tree.iter(f"{prefix}scans"))
42
+
43
+ if len(scan_element_list) != 1:
44
+ exit_program_early(f"Error parsing the xml file provided. Found none or more than one scan groups")
45
+
46
+ scans = scan_element_list[0]
47
+ quality_pairs = {int(s.get("ID")) : s.find(f"{prefix}quality").text
48
+ for s in scans}
49
+
50
+ if len(quality_pairs) == 0:
51
+ exit_program_early("Could not find scan quality information in the given xml file.")
52
+
53
+ logger.debug(f"scan quality information: {quality_pairs}")
54
+
55
+ json_paths = sorted(list(p for p in (bids_data_path / f"sub-{subject}/").rglob("*.json"))) # if re.search(json_re, p.as_posix()) != None)
56
+ nii_paths = sorted(list(p for p in (bids_data_path / f"sub-{subject}/").rglob("*.nii.gz"))) # if re.search(json_re, p.as_posix()) != None)
57
+
58
+ if len(json_paths) == 0 or len(nii_paths) == 0:
59
+ exit_program_early("Could not find Nifti or JSON sidecar files in the bids directory.")
60
+
61
+ if len(json_paths) != len(nii_paths):
62
+ exit_program_early("Unequal amount of NIFTI and JSON files found")
63
+
64
+ for p_json, p_nii in zip(json_paths, nii_paths):
65
+ j = json.load(p_json.open())
66
+ if quality_pairs[j["SeriesNumber"]] == "unusable":
67
+ logger.info(f" Removing series {j['SeriesNumber']}: NIFTI:{p_nii}, JSON:{p_json}")
68
+ os.remove(p_json)
69
+ os.remove(p_nii)
70
+
71
+
72
+ @debug_logging
73
+ def run_dcm2bids(subject:str,
74
+ session:str,
75
+ source_dir:Path,
76
+ bids_output_dir:Path,
77
+ config_file:Path,
78
+ nordic_config:Path=None,
79
+ nifti:bool=False):
80
+ """
81
+ Run dcm2bids with a given set of parameters.
82
+
83
+ :param subject: Subject name (ex. 'sub-5000', subject would be '5000')
84
+ :type subject: str
85
+ :param session: Session name (ex. 'ses-01', session would be '01')
86
+ :type session: str
87
+ :param source_dir: Path to 'sourcedata' directory (or wherever DICOM data is kept).
88
+ :type source_dir: pathlib.Path
89
+ :param bids_output_dir: Path to the bids directory to store the newly made NIFTI files
90
+ :type bids_output_dir: pathlib.Path
91
+ :param config_file: Path to dcm2bids config file, which maps raw sourcedata to BIDS-compliant counterpart
92
+ :type config_file: pathlib.Path
93
+ :param nordic_config: Path to second dcm2bids config file, needed for additional post processing that one BIDS config file can't handle.
94
+ :type nordic_config: pathlib.Path
95
+ :param nifti: Specify that the soure directory contains NIFTI files instead of DICOM
96
+ :type nifti: bool
97
+ :raise RuntimeError: If dcm2bids exits with a non-zero exit code.
98
+ """
99
+
100
+ for p in [source_dir, bids_output_dir, config_file]:
101
+ if not p.exists():
102
+ exit_program_early(f"Path {str(p)} does not exist.")
103
+
104
+ if shutil.which('dcm2bids') == None:
105
+ exit_program_early("Cannot locate program 'dcm2bids', make sure it is in your PATH.")
106
+
107
+ tmp_path = bids_output_dir / f"tmp_dcm2bids/sub-{subject}_ses-{session}"
108
+
109
+ def clean_up(quiet=False):
110
+ try:
111
+ logger.debug(f"removing the temporary directory used by dcm2bids: {tmp_path}")
112
+ shutil.rmtree(tmp_path)
113
+ except Exception:
114
+ if not quiet:
115
+ logger.warning(f"There was a problem deleting the temporary directory at {tmp_path}")
116
+
117
+ if (path_that_exists := bids_output_dir/f"sub-{subject}/ses-{session}").exists():
118
+ ans = prompt_user_continue(dedent(f"""
119
+ A raw data bids path for this subject and session already exists.
120
+ Would you like to delete its contents and rerun dcm2bids? If not,
121
+ dcm2bids will be skipped.
122
+ """))
123
+ if ans:
124
+ logger.debug("removing the old BIDS raw data directory and its contents")
125
+ shutil.rmtree(path_that_exists)
126
+ else:
127
+ return
128
+
129
+ nifti_path = None
130
+ if not nifti:
131
+ clean_up(quiet=True)
132
+ run_dcm2niix(source_dir=source_dir,
133
+ tmp_nifti_dir=tmp_path)
134
+ nifti_path = tmp_path
135
+ else:
136
+ nifti_path = source_dir
137
+ helper_command = shlex.split(f"""{shutil.which('dcm2bids')}
138
+ --bids_validate
139
+ --skip_dcm2niix
140
+ -d {str(nifti_path)}
141
+ -p {subject}
142
+ -s {session}
143
+ -c {str(config_file)}
144
+ -o {str(bids_output_dir)}
145
+ """)
146
+ try:
147
+ log_linebreak()
148
+ logger.info("####### Running first round of Dcm2Bids ########\n")
149
+ prepare_subprocess_logging(logger)
150
+ with subprocess.Popen(helper_command, stdout=subprocess.PIPE) as p:
151
+ while p.poll() == None:
152
+ for line in p.stdout:
153
+ logger.info(line.decode("utf-8", "ignore"))
154
+ prepare_subprocess_logging(logger, stop=True)
155
+ p.kill()
156
+ if p.poll() != 0:
157
+ raise RuntimeError("'dcm2bids' has ended with a non-zero exit code.")
158
+
159
+ if nordic_config:
160
+ if not nordic_config.exists():
161
+ exit_program_early(f"Path {nordic_config} does not exist.")
162
+
163
+ nordic_run_command = shlex.split(f"""{shutil.which('dcm2bids')}
164
+ --bids_validate
165
+ --skip_dcm2niix
166
+ -d {str(nifti_path)}
167
+ -p {subject}
168
+ -s {session}
169
+ -c {str(nordic_config)}
170
+ -o {str(bids_output_dir)}
171
+ """)
172
+ log_linebreak()
173
+ logger.info("####### Running second round of Dcm2Bids ########\n")
174
+ prepare_subprocess_logging(logger)
175
+ with subprocess.Popen(nordic_run_command, stdout=subprocess.PIPE) as p:
176
+ while p.poll() == None:
177
+ for line in p.stdout:
178
+ logger.info(line.decode("utf-8", "ignore"))
179
+ prepare_subprocess_logging(logger, stop=True)
180
+ p.kill()
181
+ if p.poll() != 0:
182
+ raise RuntimeError("'dcm2bids' has ended with a non-zero exit code.")
183
+
184
+ # Clean up NORDIC files
185
+ if not flags.debug:
186
+ separate_nordic_files = glob(f"{str(bids_output_dir)}/sub-{subject}/ses-{session}/func/*_part-*")
187
+ logger.debug(f"removing the old nordic files that are not needed after mag-phase combination :\n {separate_nordic_files}")
188
+ for f in separate_nordic_files:
189
+ os.remove(f)
190
+
191
+ except RuntimeError or subprocess.CalledProcessError as e:
192
+ prepare_subprocess_logging(logger, stop=True)
193
+ logger.exception(e, stack_info=True)
194
+ exit_program_early("Problem running 'dcm2bids'.", None if flags.debug else clean_up)
195
+ if not flags.debug:
196
+ clean_up()
197
+
198
+
199
+ @debug_logging
200
+ def run_dcm2niix(source_dir:Path,
201
+ tmp_nifti_dir:Path,
202
+ clean_up_func=None):
203
+ """
204
+ Run dcm2niix with the given input and output directories.
205
+
206
+ :param source_dir: Path to 'sourcedata' directory (or wherever DICOM data is kept).
207
+ :type source_dir: pathlib.Path
208
+ :param tmp_nifti_dir: Path to the directory to store the newly made NIFTI files
209
+ :type tmp_nifti_dir: pathlib.Path
210
+ """
211
+
212
+ if not source_dir.exists():
213
+ exit_program_early(f"Path {source_dir} does not exist.")
214
+ elif shutil.which('dcm2niix') == None:
215
+ exit_program_early("Cannot locate program 'dcm2niix', make sure it is in your PATH.")
216
+
217
+ if not tmp_nifti_dir.exists():
218
+ tmp_nifti_dir.mkdir(parents=True)
219
+
220
+ helper_command = shlex.split(f"""{shutil.which('dcm2niix')}
221
+ -b y
222
+ -ba y
223
+ -z y
224
+ -f %3s_%f_%p_%t
225
+ -o {str(tmp_nifti_dir)}
226
+ {str(source_dir)}
227
+ """)
228
+ try:
229
+ log_linebreak()
230
+ logger.info("####### Converting DICOM files into NIFTI #######\n")
231
+ prepare_subprocess_logging(logger)
232
+ with subprocess.Popen(helper_command, stdout=subprocess.PIPE) as p:
233
+ while p.poll() == None:
234
+ for line in p.stdout:
235
+ logger.info(line.decode("utf-8", "ignore"))
236
+ prepare_subprocess_logging(logger, stop=True)
237
+ p.kill()
238
+ if p.poll() != 0:
239
+ raise RuntimeError("'dcm2bniix' has ended with a non-zero exit code.")
240
+ except RuntimeError or subprocess.CalledProcessError as e:
241
+ prepare_subprocess_logging(logger, stop=True)
242
+ logger.exception(e, stack_info=True)
243
+ exit_program_early("Problem running 'dcm2niix'.",
244
+ exit_func=clean_up_func if clean_up_func else None)
245
+
246
+ # Delete or move extra files from short runs
247
+ files_to_remove = list(tmp_nifti_dir.glob("*a.nii.gz")) + list(tmp_nifti_dir.glob("*a.json"))
248
+ if flags.debug:
249
+ unused_files_dir = tmp_nifti_dir.parent / f"{tmp_nifti_dir.name}_unused"
250
+ unused_files_dir.mkdir(exist_ok=True)
251
+ logger.debug(f"moving some unused files and files created from shortened runs to directory {unused_files_dir}")
252
+ for f in files_to_remove:
253
+ shutil.move(f.resolve(), (unused_files_dir/f.name).resolve())
254
+ else:
255
+ logger.info(f"removing some unused files and files created from shortened runs :\n {[str(f) for f in files_to_remove]}")
256
+ for f in files_to_remove:
257
+ os.remove(f)
258
+
259
+
260
+ @debug_logging
261
+ def dicom_to_bids(subject:str,
262
+ session:str,
263
+ source_dir:Path,
264
+ bids_dir:Path,
265
+ xml_path:Path,
266
+ bids_config:Path,
267
+ nordic_config:Path=None,
268
+ nifti=False):
269
+
270
+ """
271
+ Facilitates the conversion of DICOM data into NIFTI data in BIDS format, and the removal of data marked 'unusable'.
272
+
273
+ :param subject: Subject name (ex. 'sub-5000', subject would be '5000')
274
+ :type subject: str
275
+ :param session: Session name (ex. 'ses-01', session would be '01')
276
+ :type session: str
277
+ :param source_dir: Path to 'sourcedata' directory (or wherever DICOM data is kept).
278
+ :type source_dir: pathlib.Path
279
+ :param bids_dir: Path to the bids directory to store the newly made NIFTI files
280
+ :type bids_dir: pathlib.Path
281
+ :param bids_config: Path to dcm2bids config file, which maps raw sourcedata to BIDS-compliant counterpart
282
+ :type bids_config: pathlib.Path
283
+ :param nordic_config: Path to second dcm2bids config file, needed for additional post processing if NORDIC data that one BIDS config file can't handle.
284
+ :type nordic_config: pathlib.Path
285
+ :param nifti: Specify that the soure directory contains NIFTI files instead of DICOM
286
+ :type nifti: bool
287
+ """
288
+
289
+ run_dcm2bids(subject=subject,
290
+ session=session,
291
+ source_dir=source_dir,
292
+ bids_output_dir=bids_dir,
293
+ config_file=bids_config,
294
+ nordic_config=nordic_config,
295
+ nifti=nifti)
296
+
297
+ remove_unusable_runs(xml_file=xml_path,
298
+ bids_data_path=bids_dir,
299
+ subject=subject)
300
+
301
+
302
+ if __name__ == "__main__":
303
+ parser = ArgumentParser(prog="bids_wrapper.py",
304
+ description="wrapper script for dcm2bids",
305
+ epilog="WIP")
306
+ parser.add_argument("-su", "--subject", required=True,
307
+ help="Subject ID")
308
+ parser.add_argument("-se","--session", required=True,
309
+ help="Session ID")
310
+ parser.add_argument("-sd", "--source_data", type=Path, required=True,
311
+ help="Path to directory containing this session's DICOM files")
312
+ parser.add_argument("-b", "--bids_path", type=Path, required=True,
313
+ help="Path to the bids directory to store the newly made NIFTI files")
314
+ parser.add_argument("-x", "--xml_path", type=Path, required=True,
315
+ help="Path to this session's XML file")
316
+ parser.add_argument("-c", "--bids_config", type=Path, required=True,
317
+ help="dcm2bids config json file")
318
+ parser.add_argument("-n", "--nordic_config", type=Path,
319
+ help="Second dcm2bids config json file used for NORDIC processing")
320
+ parser.add_argument("--nifti", action='store_true',
321
+ help="Flag to specify that the source directory contains files of type NIFTI (.nii/.jsons) instead of DICOM")
322
+ args = parser.parse_args()
323
+
324
+ dicom_to_bids(subject=args.subject,
325
+ session=args.session,
326
+ source_dir=args.source_data,
327
+ bids_dir=args.bids_path,
328
+ xml_path=args.xml_path,
329
+ bids_config=args.bids_config,
330
+ nordic_config=args.nordic_config,
331
+ nifti=args.nifti)
@@ -0,0 +1,153 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ import nibabel as nib
4
+ import json
5
+ import os
6
+ from .utils import exit_program_early, debug_logging, log_linebreak
7
+ from glob import glob
8
+ from pathlib import Path
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ def find_nearest(array, value):
14
+ """
15
+ Finds the smallest difference in 'value' and one of the
16
+ elements of 'array', and returns the index of the element
17
+
18
+ :param array: a list of elements to compare value to
19
+ :type array: a list or list-like object
20
+ :param value: a value to compare to elements of array
21
+ :type value: integer or float
22
+ :return: integer index of array
23
+ :rtype: int
24
+ """
25
+ array = np.asarray(array)
26
+ idx = (np.abs(array - value)).argmin()
27
+ return(array[idx])
28
+
29
+
30
+ @debug_logging
31
+ def make_events_long(bold_run:Path, event_file:Path, output_file:Path, tr:float):
32
+ """
33
+ Takes and event file and a funtional run and creates a long formatted events file
34
+ that maps the onset of task events to a frame of the functional run
35
+
36
+ :param bold_run: path to the function run corresponding to the event file
37
+ :type bold_run: pathlib.Path
38
+ :param event_file: path to the event timing file
39
+ :type event_file: pathlib.Path
40
+ :param output_file: file path (including name) to save the long formatted event file to
41
+ :type output_file: pathlib.Path
42
+ :param tr: Repetition time of the function run in seconds
43
+ :type tr: float
44
+ """
45
+ nvols = nib.load(bold_run).dataobj.shape[-1]
46
+ duration = nvols * tr
47
+
48
+ events_df = pd.read_csv(event_file, index_col=None, delimiter="\t")
49
+ conditions = [s for s in np.unique(events_df.trial_type)]
50
+ events_long = pd.DataFrame(0, columns=conditions, index=np.arange(0,duration,tr))
51
+
52
+ for e in events_df.index:
53
+ i = find_nearest(events_long.index, events_df.loc[e, "onset"])
54
+ events_long.loc[i, events_df.loc[e, "trial_type"]] = 1
55
+ if events_df.loc[e, "duration"] > tr:
56
+ offset = events_df.loc[e, "onset"] + events_df.loc[e, "duration"]
57
+ j = find_nearest(events_long.index, offset)
58
+ if j>i:
59
+ events_long.loc[j, events_df.loc[e, "trial_type"]] = 1
60
+
61
+ events_long.to_csv(output_file)
62
+
63
+
64
+ @debug_logging
65
+ def append_to_confounds(confounds_file:Path, fd_thresh:float):
66
+ """
67
+ Makes motion outlier regressors based on the framewise
68
+ displacement threshold, and appends them to the confounds file
69
+ for this functional run
70
+
71
+ :param confounds_file: path to the confounds file for this functional run
72
+ :type confounds_file: pathlib.Path
73
+ :param fd_thresh: Framewise displacement threshold for this functional run
74
+ :type fd_thresh: float
75
+ """
76
+ conf_df = pd.read_csv(confounds_file, delimiter="\t")
77
+ b = 0
78
+ for a in range(len(conf_df)):
79
+ if conf_df.loc[a, "framewise_displacement"] > fd_thresh:
80
+ conf_df[f"spike{b}"] = 0
81
+ conf_df.loc[a, f"spike{b}"] = 1
82
+ b += 1
83
+
84
+ conf_df.to_csv(confounds_file, sep="\t")
85
+
86
+
87
+ @debug_logging
88
+ def create_events_and_confounds(bids_path:Path, derivs_path:Path, sub:str, ses:str, fd_thresh:float):
89
+ """
90
+ Facilitates the creation of a long formatted events file
91
+ and the appending of motion outlier regressors to the
92
+ confounds file for each functional run for this subject and session
93
+
94
+ :param bids_path: Path to the bids directory containing the raw
95
+ :type bids_path: pathlib.Path
96
+ :param derivs_path: Path to BIDS-compliant derivatives folder.
97
+ :type derivs_path: pathlib.Path
98
+ :param subject: Subject id (
99
+ :type subject: str
100
+ :param session: Session id
101
+ :type session: str
102
+ :param fd_thresh: Framewise displacement threshold for this functional run
103
+ :type fd_thresh: float
104
+
105
+ """
106
+ log_linebreak()
107
+ logger.info("####### Creating long formatted event files ########\n")
108
+
109
+ bids_func = bids_path / f"sub-{sub}/ses-{ses}/func"
110
+ derivs_func = derivs_path / f"sub-{sub}/ses-{ses}/func"
111
+ if not bids_func.is_dir():
112
+ exit_program_early(f"Cannnot find 'func' - {bids_func} - bids directory for this subject and session")
113
+ if not derivs_func.is_dir():
114
+ exit_program_early(f"Cannnot find 'func' - {derivs_func} - derivatives directory for this subject and session")
115
+
116
+ event_time_files = list(bids_func.glob("*_events.tsv"))
117
+ logger.info(f"Found {len(event_time_files)} event timing files")
118
+ for etf in event_time_files:
119
+ search_path = f"/sub-{sub}_ses-{ses}*"
120
+ task = etf.name.split('task-')[-1].split('_')[0]
121
+ search_path = f"{search_path}task-{task}*"
122
+ run = None
123
+ if "run" in etf.name:
124
+ run = etf.name.split('run-')[-1].split('_')[0].zfill(2)
125
+ search_path = f"{search_path}run-{run}*"
126
+ bold_file = list(bids_func.glob(f"{search_path}bold.nii*"))
127
+ if len(bold_file) < 1:
128
+ logger.info(f"Could not find any bold files that matched this event timing file: {etf}")
129
+ continue
130
+ confounds_file = list(derivs_func.glob(f"{search_path}confounds_timeseries.tsv"))
131
+ if len(confounds_file) < 1:
132
+ logger.info(f"Could not find any confounds files that matched this event timing file: {etf}")
133
+ continue
134
+ confounds_file = confounds_file[0]
135
+ bold_file = bold_file[0]
136
+ side_car = bold_file.split(".")[0] + ".json"
137
+ tr = None
138
+ with open(side_car, "r") as f:
139
+ jd = json.load(f)
140
+ tr = jd["RepetitionTime"]
141
+
142
+ event_file_out = derivs_func / f"sub-{sub}_ses-{ses}_task-{task}_{f'run-{run}' if run else ''}_desc-events_long.csv"
143
+ make_events_long(bold_run=bold_file,
144
+ event_file=etf,
145
+ output_file=event_file_out,
146
+ tr=tr)
147
+ append_to_confounds(confounds_file=confounds_file,
148
+ fd_thresh=fd_thresh)
149
+
150
+
151
+
152
+
153
+
File without changes