oceanproc 0.1.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.
Files changed (41) hide show
  1. oceanproc/__init__.py +1 -0
  2. oceanproc/bids_wrapper.py +331 -0
  3. oceanproc/events_long.py +153 -0
  4. oceanproc/firstlevel/__init__.py +0 -0
  5. oceanproc/firstlevel/run_glm.py +923 -0
  6. oceanproc/firstlevel/tests/__init__.py +0 -0
  7. oceanproc/firstlevel/tests/test_run_glm.py +165 -0
  8. oceanproc/fmriprep_wrapper.py +222 -0
  9. oceanproc/group_series.py +228 -0
  10. oceanproc/grouplevel/__init__.py +0 -0
  11. oceanproc/grouplevel/computations.py +80 -0
  12. oceanproc/grouplevel/design_mat_utils.py +54 -0
  13. oceanproc/grouplevel/dscalar_utils.py +51 -0
  14. oceanproc/grouplevel/run.py +18 -0
  15. oceanproc/grouplevel/utils.py +4 -0
  16. oceanproc/ocean_proc.py +215 -0
  17. oceanproc/oceanparse.py +107 -0
  18. oceanproc/utils.py +224 -0
  19. oceanproc/web/TODO.md +4 -0
  20. oceanproc/web/__init__.py +0 -0
  21. oceanproc/web/app.py +78 -0
  22. oceanproc/web/config.json +7 -0
  23. oceanproc/web/get_parser_json.py +58 -0
  24. oceanproc/web/static/components/ArgparseForm.js +9 -0
  25. oceanproc/web/static/components/ArgparseFormEntry.js +52 -0
  26. oceanproc/web/static/components/DirIcon.js +11 -0
  27. oceanproc/web/static/components/FileBrowser.js +85 -0
  28. oceanproc/web/static/components/FileBrowserOpenBtn.js +12 -0
  29. oceanproc/web/static/components/FileEntry.js +32 -0
  30. oceanproc/web/static/components/FileIcon.js +11 -0
  31. oceanproc/web/static/components/OceanprocForm.js +53 -0
  32. oceanproc/web/static/components/styles.css +44 -0
  33. oceanproc/web/templates/filesystem.html +13 -0
  34. oceanproc/web/templates/filesystem_fail.html +2 -0
  35. oceanproc/web/templates/oceanproc_form.html +23 -0
  36. oceanproc/web/templates/react-test.html +0 -0
  37. oceanproc/web/templates/username_card_component_test.html +39 -0
  38. oceanproc-0.1.0.dist-info/METADATA +29 -0
  39. oceanproc-0.1.0.dist-info/RECORD +41 -0
  40. oceanproc-0.1.0.dist-info/WHEEL +4 -0
  41. oceanproc-0.1.0.dist-info/entry_points.txt +4 -0
File without changes
@@ -0,0 +1,165 @@
1
+ import random
2
+ import re
3
+ import itertools
4
+ from ..run_glm import *
5
+ import pytest
6
+ import pandas as pd
7
+ import numpy as np
8
+ from scipy import signal
9
+
10
+ TR = 1.2
11
+
12
+ # define some fixtures
13
+ @pytest.fixture
14
+ def confounds_df():
15
+ df = pd.DataFrame(data={"framewise_displacement": np.random.rand(50),
16
+ "global_signal": np.random.rand(50) * 5000,
17
+ "volterra": np.ones(50)})
18
+ return df
19
+
20
+
21
+ @pytest.fixture
22
+ def confounds_columns(confounds_df):
23
+ cols = [col for col in confounds_df.columns if "volterra" not in col]
24
+ return cols
25
+
26
+
27
+ @pytest.fixture
28
+ def random_fdata():
29
+ return np.random.rand(50, 91282)
30
+
31
+
32
+ @pytest.fixture
33
+ def random_eventdf():
34
+ d = {"trial_type": random.choices(('a','b','c'), k=50), #<-- 50 random a, b, or c
35
+ "onset": np.linspace(0, 60, 50), #<-- 50 intervals of 1.2 seconds}
36
+ "duration": np.ones(50)} #<-- 50 random correct, incorrect
37
+ return pd.DataFrame(data=d)
38
+
39
+ @pytest.fixture
40
+ def random_signaldf():
41
+ d = {"flat_signal": np.ones(50) * 50,
42
+ "random_signal": np.random.rand(50) * 50,
43
+ "sine_signal": np.sin(np.linspace(-np.pi, np.pi, 50))}
44
+ return pd.DataFrame(data=d)
45
+
46
+ def test_make_option():
47
+ assert make_option(True) == " "
48
+ assert make_option(True, key="myopt") == "--myopt "
49
+ assert make_option(['1','2','3']) == " 1 2 3"
50
+ assert make_option("hello") == " hello"
51
+ assert make_option("hello", key="myopt") == "--myopt hello"
52
+
53
+
54
+ @pytest.mark.parametrize("demean,linear_trend,spike_threshold,volterra_expansion,volterra_columns",
55
+ [(False, False, None, None, None),
56
+ (True, False, None, None, None),
57
+ (False, True, None, None, None),
58
+ (True, True, None, None, None),
59
+ (True, True, 0.5, None, None),
60
+ (True, True, 0.5, 1, ["volterra"]),
61
+ (True, True, 0.5, 3, ["volterra"])])
62
+ def test_make_noise_ts(tmp_path,
63
+ confounds_df,
64
+ confounds_columns,
65
+ demean,
66
+ linear_trend,
67
+ spike_threshold,
68
+ volterra_expansion,
69
+ volterra_columns):
70
+ confounds_file = tmp_path / "tmp.tsv"
71
+ confounds_df.to_csv(confounds_file, sep='\t')
72
+ noise_ts = make_noise_ts(confounds_file,
73
+ confounds_columns,
74
+ demean,
75
+ linear_trend,
76
+ spike_threshold,
77
+ volterra_expansion,
78
+ volterra_columns)
79
+ if spike_threshold:
80
+ assert spike_threshold > 0
81
+ spike_column_num = (noise_ts.loc[:, "framewise_displacement"] > spike_threshold).sum()
82
+ assert len([col for col in noise_ts.columns if "spike" in col]) == spike_column_num
83
+
84
+ assert "volterra_expansion" in locals() and "volterra_columns" in locals()
85
+ if volterra_expansion:
86
+ for i in range(volterra_expansion):
87
+ # Check that each volterra column has been shifted appropriately
88
+ assert (noise_ts.loc[0:i, f"volterra_{i+1}"] == 0).all()
89
+
90
+ @pytest.mark.parametrize("fir,hrf,fir_list,hrf_list",
91
+ [(None, None, None, None),
92
+ (3, None, ['a', 'b'], None),
93
+ (3, None, None, None),
94
+ (3, (5, 10), None, None),
95
+ (3, (5, 10), ['a','b'], None),
96
+ (3, (5, 10), None, ['a','b']),
97
+ (3, (5, 10), ['a','b'], ['a','b'])])
98
+ def test_events_to_design(tmp_path,
99
+ random_fdata,
100
+ random_eventdf,
101
+ fir,
102
+ hrf,
103
+ fir_list,
104
+ hrf_list):
105
+ def run():
106
+ return events_to_design(func_data,
107
+ TR,
108
+ event_file,
109
+ fir=fir,
110
+ hrf=hrf,
111
+ fir_list=fir_list,
112
+ hrf_list=hrf_list)
113
+ func_data = random_fdata
114
+ event_file = tmp_path / "events.tsv"
115
+ random_eventdf.to_csv(event_file, sep='\t')
116
+ # both fir and hrf specified, but no list of columns for either
117
+ if (fir and hrf) and not (fir_list or hrf_list):
118
+ with pytest.raises(RuntimeError):
119
+ events_df, run_conditions = run()
120
+ return
121
+ # fir_list, hrf_list have overlapping column names
122
+ elif (fir_list and hrf_list) and not set(fir_list).isdisjoint(hrf_list):
123
+ with pytest.raises(RuntimeError):
124
+ events_df, run_conditions = run()
125
+ return
126
+ events_df, run_conditions = run()
127
+ if fir_list: # Check if a_00, a_01, ... a_nn exist, and if the number of a columns matches the fir int
128
+ pattern = re.compile(f"{fir_list[0]}_\\d\\d")
129
+ assert len([col for col in events_df.columns if re.match(pattern, col)]) == fir
130
+
131
+
132
+ @pytest.mark.parametrize("demean,linear_trend,spike_threshold,volterra_expansion,volterra_columns",
133
+ [(False, False, None, None, None),
134
+ (True, False, None, None, None),
135
+ (False, True, None, None, None),
136
+ (True, True, None, None, None),
137
+ (True, True, 0.5, None, None),
138
+ (True, True, 0.5, 1, ["volterra"]),
139
+ (True, True, 0.5, 3, ["volterra"])])
140
+ def test_nuisance_regression(tmp_path,
141
+ confounds_df,
142
+ confounds_columns,
143
+ random_fdata,
144
+ demean,
145
+ linear_trend,
146
+ spike_threshold,
147
+ volterra_expansion,
148
+ volterra_columns):
149
+ confounds_file = tmp_path / "tmp.tsv"
150
+ confounds_df.to_csv(confounds_file, sep='\t')
151
+ noise_ts = make_noise_ts(confounds_file,
152
+ confounds_columns,
153
+ demean,
154
+ linear_trend,
155
+ spike_threshold,
156
+ volterra_expansion,
157
+ volterra_columns)
158
+ data_minus_residuals = nuisance_regression(random_fdata, noise_ts)
159
+
160
+
161
+ # def test_hrf_convolve_features(tmp_path,
162
+ # random_signaldf):
163
+ # cfeats = hrf_convolve_features(random_signaldf)
164
+ # for col in random_signaldf.columns:
165
+ # recovered, remainder = signal.deconvolve(cfeats[col], )
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from pathlib import Path
4
+ import logging
5
+ from .utils import exit_program_early, make_option, prepare_subprocess_logging, flags, debug_logging, log_linebreak
6
+ import shlex
7
+ import shutil
8
+ from subprocess import Popen, PIPE
9
+ from matplotlib import pyplot as plt
10
+ from bs4 import BeautifulSoup as bsoup
11
+ from pathlib import Path
12
+ import pandas as pd
13
+ import numpy as np
14
+ import json
15
+ import os
16
+ import copy
17
+
18
+ logger = logging.getLogger(__name__)
19
+ plt.set_loglevel("warning")
20
+
21
+ @debug_logging
22
+ def run_fmri_prep(subject:str,
23
+ bids_path:Path,
24
+ derivs_path:Path,
25
+ option_chain:str,
26
+ remove_work_folder:Path=None):
27
+ """
28
+ Run fmriprep with parameters.
29
+
30
+ :param subject: Name of subject (ex. if path contains 'sub-5000', subject='5000')
31
+ :type subject: str
32
+ :param bids_path: Path to BIDS-compliant raw data folder.
33
+ :type bids_path: pathlib.Path
34
+ :param derivs_path: Path to BIDS-compliant derivatives folder.
35
+ :type derivs_path: pathlib.Path
36
+ :param option_chain: String containing generated list of options built by make_option().
37
+ :type option_chain: str
38
+ :param remove_work_folder: Path to the working directory that will be deleted upon completion or error (default None)
39
+ :type remove_work_folder: str
40
+ :raise RuntimeError: If fmriprep throws an error, or exits with a non-zero exit code.
41
+ """
42
+ clean_up = lambda : shutil.rmtree(remove_work_folder) if remove_work_folder else None
43
+
44
+ log_linebreak()
45
+ logger.info("####### Starting fMRIPrep #######\n")
46
+ if not bids_path.exists():
47
+ exit_program_early(f"Bids path {bids_path} does not exist.")
48
+ elif not derivs_path.exists():
49
+ exit_program_early(f"Derivatives path {derivs_path} does not exist.")
50
+ elif shutil.which('fmriprep-docker') == None:
51
+ exit_program_early("Cannot locate program 'fmriprep-docker', make sure it is in your PATH.")
52
+
53
+
54
+
55
+ uid = Popen(["id", "-u"], stdout=PIPE).stdout.read().decode("utf-8").strip()
56
+ gid = Popen(["id", "-g"], stdout=PIPE).stdout.read().decode("utf-8").strip()
57
+ cifti_out_res = '91k'
58
+
59
+ helper_command = shlex.split(f"""{shutil.which('fmriprep-docker')}
60
+ --user {uid}:{gid}
61
+ --participant-label={subject}
62
+ --cifti-output={cifti_out_res}
63
+ --use-syn-sdc=warn
64
+ {option_chain}
65
+ {str(bids_path)}
66
+ {str(derivs_path)}
67
+ participant""")
68
+ try:
69
+ command_str = " ".join(helper_command)
70
+ logger.info(f"Running fmriprep-docker with the following command: \n {command_str} \n")
71
+ prepare_subprocess_logging(logger)
72
+ with Popen(helper_command, stdout=PIPE) as p:
73
+ while p.poll() == None:
74
+ for line in p.stdout:
75
+ logger.info(line.decode("utf-8", "ignore"))
76
+ prepare_subprocess_logging(logger, stop=True)
77
+ p.kill()
78
+ if p.poll() != 0:
79
+ raise RuntimeError("'fmriprep-docker' has ended with a non-zero exit code.")
80
+ except RuntimeError as e:
81
+ prepare_subprocess_logging(logger, stop=True)
82
+ logger.exception(e, stack_info=True)
83
+ exit_program_early("Program 'fmriprep-docker' has run into an error.",
84
+ None if flags.debug else clean_up)
85
+ if not flags.debug:
86
+ clean_up()
87
+
88
+
89
+ @debug_logging
90
+ def add_fd_plot_to_report(subject:str,
91
+ session:str,
92
+ derivs_path:Path):
93
+ """
94
+ Reads each confounds file in the fmriprep functional output, plots the framewise
95
+ displacement, and adds this figure into the output report
96
+
97
+ :param subject: Name of subject (ex. if path contains 'sub-5000', subject='5000')
98
+ :type subject: str
99
+ :param session: Session name (ex. 'ses-01', session would be '01')
100
+ :type session: str
101
+ :param derivs_path: Path to BIDS-compliant derivatives folder.
102
+ :type derivs_path: pathlib.Path
103
+ """
104
+
105
+ func_path = derivs_path / f"sub-{subject}" / f"ses-{session}" / "func"
106
+ figures_path = derivs_path / f"sub-{subject}" / "figures"
107
+ report_path = derivs_path / f"sub-{subject}.html"
108
+
109
+ for p in [func_path, figures_path, report_path]:
110
+ if not p.exists():
111
+ exit_program_early(f"Expected Path {str(p)} does not exist.")
112
+
113
+ log_linebreak()
114
+ logger.info("####### Appending FD Plots to fMRIPrep Report #######\n")
115
+
116
+ logger.debug(f"parsing the fMRIPrep html report file: {report_path}")
117
+ report_file = open(report_path, "r")
118
+ soup = bsoup(report_file.read(), 'html.parser')
119
+ report_file.close()
120
+
121
+ for f in func_path.glob("*desc-confounds_timeseries.tsv"):
122
+ logger.info(f"plotting framewise displacement from confounds file:{f}")
123
+ try :
124
+ run = f.name.split("run-")[-1].split("_")[0]
125
+ task = f.name.split("task-")[-1].split("_")[0]
126
+
127
+ # read the repetition time from the json files for the BOLD data
128
+ bold_js = list(f.parent.glob(f"*task-{task}*run-{run}*bold.json"))[0]
129
+ logger.debug(f" reading repetition time from JSON file: {bold_js}")
130
+ tr = None
131
+ with open(bold_js, "r") as jf:
132
+ tr = float(json.load(jf)["RepetitionTime"])
133
+
134
+ # read in the confounds file
135
+ confound_df = pd.read_csv(f, sep="\t")
136
+ n_frames = len(confound_df["framewise_displacement"])
137
+ x_vals = np.arange(0, n_frames*tr, tr)
138
+ mean_fd = np.mean(confound_df["framewise_displacement"])
139
+ func_thresh = 0.9
140
+ rest_thresh = 0.2
141
+
142
+ # plot the framewise displacement
143
+ fig, ax = plt.subplots(1,1, figsize=(15,5))
144
+ # ax.set_ylabel("Displacement (mm)")
145
+ ax.set_xlabel("Time (sec)")
146
+ ax.plot(x_vals, confound_df["framewise_displacement"], label="FD Trace")
147
+ ax.plot(x_vals, [func_thresh]*n_frames, label=f"Threshold: {func_thresh}")
148
+ ax.plot(x_vals, [rest_thresh]*n_frames, label=f"Threshold: {rest_thresh}")
149
+ ax.plot(x_vals, [mean_fd]*n_frames, label=f"Mean: {round(mean_fd,2)}")
150
+ ax.set_xlim(0, (n_frames*tr))
151
+ ax.set_ylim(0, 1.5)
152
+ ax.legend(loc="upper left")
153
+ plot_path = figures_path / f"sub-{subject}_ses-{session}_task-{task}_run-{run}_desc-fd-trace.svg"
154
+ fig.savefig(plot_path, bbox_inches="tight", format="svg", pad_inches=0.2)
155
+ logger.debug(f" saved the fd plot figure for run-{run} to path: {plot_path}")
156
+
157
+ # find the location in the report where the new figure will go
158
+ confounds_plot_div = soup.find(id=lambda x: (f"desc-carpetplot_run-{run}" in x) if x else False)
159
+
160
+ # Copy a div element from the report and add the new figure into it
161
+ fd_plot_div = copy.copy(confounds_plot_div)
162
+ del fd_plot_div["id"]
163
+ fd_plot_div.p.extract()
164
+ fd_plot_div.h3.string = "Scaled FD Plot"
165
+ rel_path = os.path.relpath(plot_path, derivs_path)
166
+ fd_plot_div.img["src"] = "./" + rel_path
167
+
168
+ # find the reference div for the copied div element and make a copy of this as well
169
+ confounds_plot_reference_div = confounds_plot_div.find_next_sibling("div", class_="elem-filename")
170
+
171
+ fd_plot_reference_div = copy.copy(confounds_plot_reference_div)
172
+ fd_plot_reference_div.a["href"] = "./" + rel_path
173
+ fd_plot_reference_div.a.string = rel_path
174
+
175
+ # Add the new elements into the file
176
+ logger.debug(f" inserting the new html elements into the fMRIPrep report")
177
+ confounds_plot_reference_div.insert_after(fd_plot_div)
178
+ fd_plot_div.insert_after(fd_plot_reference_div)
179
+ except Exception as e:
180
+ logger.warning(f"Error generating the scaled FD plot for confound file: {f}")
181
+
182
+ logger.info("writing the edited html to the report file")
183
+ with open(report_path, "w") as f:
184
+ f.write(soup.prettify())
185
+
186
+
187
+ @debug_logging
188
+ def process_data(subject:str,
189
+ session:str,
190
+ bids_path:Path,
191
+ derivs_path:Path,
192
+ remove_work_folder:Path=None,
193
+ **kwargs):
194
+ """
195
+ Faciliates the running of fmriprep and any additions to the output report.
196
+
197
+ :param subject: Name of subject (ex. if path contains 'sub-5000', subject='5000')
198
+ :type subject: str
199
+ :param session: Session name (ex. 'ses-01', session would be '01')
200
+ :type session: str
201
+ :param bids_path: Path to BIDS-compliant raw data folder.
202
+ :type bids_path: pathlib.Path
203
+ :param derivs_path: Path to BIDS-compliant derivatives folder.
204
+ :type derivs_path: pathlib.Path
205
+ :param remove_work_folder: Path to the working directory that will be deleted upon completion or error (default None)
206
+ :type remove_work_folder: str
207
+ :param **kwargs: any arguments to be passed to the fmriprep subprocess
208
+ """
209
+
210
+ fmriprep_option_chain = " ".join([make_option(v, key=k, delimeter="=", convert_underscore=True) for k,v in kwargs.items()])
211
+
212
+ run_fmri_prep(subject=subject,
213
+ bids_path=bids_path,
214
+ derivs_path=derivs_path,
215
+ option_chain=fmriprep_option_chain,
216
+ remove_work_folder=remove_work_folder)
217
+
218
+ add_fd_plot_to_report(subject=subject,
219
+ session=session,
220
+ derivs_path=derivs_path)
221
+
222
+
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import glob
4
+ import json
5
+ import os
6
+ from datetime import datetime
7
+ from collections import defaultdict
8
+ import argparse
9
+ import re
10
+ from pathlib import Path
11
+ import xml.etree.ElementTree as et
12
+ from .utils import exit_program_early, debug_logging, log_linebreak
13
+ import logging
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ @debug_logging
18
+ def get_locals_from_xml(xml_path: Path) -> set:
19
+ """
20
+ Read in the xml file to find the localizers.
21
+
22
+ :param xml_path: Path to XML generated by XNAT
23
+ :type xml_path: pathlib.Path
24
+ :return: Set containing all localizer IDs
25
+ :rtype: set
26
+ """
27
+ tree = et.parse(xml_path)
28
+ prefix = "{" + str(tree.getroot()).split("{")[-1].split("}")[0] + "}"
29
+ scan_element_list = list(tree.iter(f"{prefix}scans"))
30
+
31
+ if len(scan_element_list) != 1:
32
+ exit_program_early(f"Error parsing the xml file provided. Found none or more than one scan groups")
33
+
34
+ scans = scan_element_list[0]
35
+
36
+ localizers = set()
37
+ for s in scans:
38
+ if re.match(r"Localizer.*", s.get("type")) and s.find(f'{prefix}quality').text == "usable":
39
+ localizers.add(int(s.get("ID")))
40
+ return sorted(localizers)
41
+
42
+
43
+ @debug_logging
44
+ def get_func_from_bids(bids_path: Path,
45
+ localizers: set[int],
46
+ json_dict: defaultdict[list],
47
+ groupings: list[dict[str:set]]):
48
+ """
49
+ Read in the JSON associated with task-based runs from the bids dir, and pair with a localizer run.
50
+
51
+ :param bids_path: path to BIDS-compliant .nii/.json files
52
+ :type bids_path: pathlib.Path
53
+ :param localizers: set of localizer IDs
54
+ :type localizers: set[int]
55
+ :param json_dict: dict with each key being a series number, each value being an object representation of it's JSON file with added 'bidsname' and 'filename' attributes
56
+ :type json_dict: defaultdict[list]
57
+ :param groupings: list of dictionaries containing mappings of localizers to series number
58
+ :type groupings: list[dict[str:set]]
59
+ """
60
+ bids_func_json = sorted(list(bids_path.glob("func/*bold.json")))
61
+ # assert len(bids_func_json) > 0, "---[ERROR]: Could not find any JSON files for bold runs in the functional directory of the bids data"
62
+ if len(bids_func_json) == 0:
63
+ exit_program_early("Could not find any JSON files for bold runs in the functional directory of the bids data.")
64
+ for jf in bids_func_json:
65
+ jd = None
66
+ with open(jf, "r") as j:
67
+ jd = json.load(j)
68
+ series_num = int(jd["SeriesNumber"])
69
+ jd["bidsname"] = f"{bids_path.name}/func/{jf.name.split('/')[-1][:-4]}nii.gz"
70
+ jd["filename"] = str(jf)
71
+ json_dict[series_num].append(jd)
72
+ for i, l in enumerate(localizers):
73
+ if i < len(localizers)-1:
74
+ if series_num > l and series_num < localizers[i+1]:
75
+ groupings[i]["task"].add(series_num)
76
+ break
77
+ else:
78
+ groupings[i]["task"].add(series_num)
79
+
80
+
81
+ # Read in the json for the field maps from the bids dir
82
+ @debug_logging
83
+ def get_fmap_from_bids(bids_path: Path,
84
+ localizers: set[int],
85
+ json_dict: defaultdict[list],
86
+ groupings: list[dict[str:set]]):
87
+ """
88
+ Read in all fmap JSON from the bids dir, and pair to a localizer.
89
+
90
+ :param bids_path: path to BIDS-compliant .nii/.json files
91
+ :type bids_path: pathlib.Path
92
+ :param localizers: set of localizer IDs
93
+ :type localizers: set[int]
94
+ :param json_dict: dict with each key being a series number, each value being an object representation of it's JSON file with added 'bidsname' and 'filename' attributes
95
+ :type json_dict: defaultdict[list]
96
+ :param groupings: list of dictionaries containing mappings of localizers to series number
97
+ :type groupings: list[dict[str:set]]
98
+ """
99
+ bids_fmap_json = sorted(list(bids_path.glob("fmap/*.json")))
100
+ # assert len(bids_fmap_json) > 0, "---[ERROR]: Could not find any JSON files from the fieldmap directory of the bids data"
101
+ if len(bids_fmap_json) == 0:
102
+ exit_program_early("Could not find any JSON files from the fieldmap directory of the bids data.")
103
+ for jf in bids_fmap_json:
104
+ jd = None
105
+ with open(jf, "r") as j:
106
+ jd = json.load(j)
107
+ series_num = int(jd["SeriesNumber"])
108
+ jd["filename"] = jf.name.split("/")[-1]
109
+ json_dict[series_num].append(jd)
110
+ direction = "fmapAP" if jd["PhaseEncodingDirection"] == "j-" else "fmapPA"
111
+ for i, l in enumerate(localizers):
112
+ if i < len(localizers)-1:
113
+ if series_num > l and series_num < localizers[i+1]:
114
+ groupings[i][direction].add(series_num)
115
+ break
116
+ else:
117
+ groupings[i][direction].add(series_num)
118
+
119
+
120
+ @debug_logging
121
+ def map_fmap_to_func(xml_path: Path,
122
+ bids_dir_path: Path):
123
+ """
124
+ Group field maps to BOLD task runs.
125
+
126
+ :param xml_path: path to XML generated by XNAT
127
+ :type xml_path: pathlib.Path
128
+ :param bids_dir_path: path to BIDS-compliant session directory
129
+ :type bids_dir_path: pathlib.Path
130
+ """
131
+ log_linebreak()
132
+ logger.info("####### Pairing field maps to functional runs #######\n")
133
+
134
+ if not xml_path.is_file():
135
+ exit_program_early(f"Session xml file {xml_path} does not exist.")
136
+ if not bids_dir_path.is_dir():
137
+ exit_program_early(f"Session bids dicrectory {bids_dir_path} does not exist.")
138
+ series_json = defaultdict(list)
139
+
140
+ locals_series = get_locals_from_xml(xml_path=xml_path)
141
+ logger.info(f"Localizers: {locals_series}")
142
+
143
+ groups = [{"task":set(), "fmapAP": set(), "fmapPA": set()} for _ in locals_series]
144
+ get_func_from_bids(bids_path=bids_dir_path,
145
+ localizers=locals_series,
146
+ json_dict=series_json,
147
+ groupings=groups)
148
+ get_fmap_from_bids(bids_path=bids_dir_path,
149
+ localizers=locals_series,
150
+ json_dict=series_json,
151
+ groupings=groups)
152
+ logger.info(f"Localizer groups: {groups}")
153
+
154
+ for group in groups:
155
+ # assert len(group["fmapAP"]) == len(group["fmapPA"]), "Unequal number of AP and PA field maps!"
156
+ if len(group["fmapAP"]) != len(group["fmapPA"]):
157
+ exit_program_early("Unequal number of AP and PA field maps.")
158
+ fmap_pairs = tuple(zip(sorted(group["fmapAP"]), sorted(group["fmapPA"])))
159
+ fmap_times = []
160
+
161
+ # get times for field maps
162
+ for i,p in enumerate(fmap_pairs):
163
+ times = sorted((datetime.strptime(series_json[p[0]][0]["AcquisitionTime"], "%H:%M:%S.%f"),
164
+ datetime.strptime(series_json[p[1]][0]["AcquisitionTime"], "%H:%M:%S.%f")))
165
+ fmap_times.append(times[0] + (abs(times[1] - times[0])/2))
166
+
167
+ # pair task runs with field maps based on closest Acquisition Time
168
+ map_pairings = {s:[] for s in fmap_pairs}
169
+ for t in group["task"]:
170
+ aqt = datetime.strptime(series_json[t][0]["AcquisitionTime"], "%H:%M:%S.%f")
171
+ diff = list(map(lambda x: abs(x-aqt), fmap_times))
172
+ pairing = fmap_pairs[diff.index(min(diff))]
173
+ map_pairings[pairing].append(t)
174
+ logger.info(f"Field map pairings: {map_pairings}")
175
+
176
+ # add the list of task run files that are paired with each field map in their json files
177
+ for k,v in map_pairings.items():
178
+ for s in k:
179
+ jd = series_json[s][0]
180
+ file = jd.pop("filename")
181
+ jd["IntendedFor"] = []
182
+ for t in v:
183
+ for echo in series_json[t]:
184
+ jd["IntendedFor"].append(echo["bidsname"])
185
+ fmap_file = bids_dir_path/f"fmap/{file}"
186
+ with open(fmap_file, "w") as out_file:
187
+ logger.debug(f"writing field map pairing information to file: {fmap_file}")
188
+ out_file.write(json.dumps(jd, indent=4))
189
+
190
+ def map_fmap_to_func_with_pairing_file(bids_dir_path: Path,
191
+ pairing_json: Path):
192
+ log_linebreak()
193
+ for json_path in bids_dir_path.glob("fmap/*json"): # reset IntendedFor field in all fmap json
194
+ with open(json_path) as f:
195
+ json_obj = json.load(f)
196
+ json_obj["IntendedFor"] = []
197
+ with open(json_path, "w") as f:
198
+ json.dump(json_obj, f, indent=4)
199
+ logger.info("####### Pairing field maps to functional runs using pairing file #######\n")
200
+ with pairing_json.open() as f:
201
+ pairing_dict = json.load(f)
202
+ pairings_list = pairing_dict["pairings"]
203
+ for pairing in pairings_list:
204
+ fmap_jsons = bids_dir_path.glob(f"fmap/*{pairing['fmap']}*json")
205
+ func_paths = []
206
+ for func in pairing["func"]:
207
+ func_paths.extend(
208
+ [str(p) for p in bids_dir_path.parent.glob(f"*/func/*{func}*nii.gz")]
209
+ )
210
+ for fmap_json in fmap_jsons:
211
+ with fmap_json.open() as f:
212
+ fmap_dict = json.load(f)
213
+ fmap_dict["IntendedFor"] = func_paths
214
+ with fmap_json.open('w') as f:
215
+ json.dump(fmap_dict, f, indent=4)
216
+
217
+
218
+ if __name__ == "__main__":
219
+ parser = argparse.ArgumentParser(
220
+ prog="group_series.py",
221
+ description="Grouping field maps to BOLD task runs"
222
+ )
223
+ parser.add_argument("xml_ses_file", type=Path, help="The path to the xml file for this session")
224
+ parser.add_argument("bids_ses_dir", type=Path, help="The path to the bids directory for this session")
225
+ args = parser.parse_args()
226
+
227
+ map_fmap_to_func(xml_path=args.xml_ses_file,
228
+ bids_dir_path=args.bids_ses_dir)
File without changes