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.
- oceanproc/__init__.py +1 -0
- oceanproc/bids_wrapper.py +331 -0
- oceanproc/events_long.py +153 -0
- oceanproc/firstlevel/__init__.py +0 -0
- oceanproc/firstlevel/run_glm.py +923 -0
- oceanproc/firstlevel/tests/__init__.py +0 -0
- oceanproc/firstlevel/tests/test_run_glm.py +165 -0
- oceanproc/fmriprep_wrapper.py +222 -0
- oceanproc/group_series.py +228 -0
- oceanproc/grouplevel/__init__.py +0 -0
- oceanproc/grouplevel/computations.py +80 -0
- oceanproc/grouplevel/design_mat_utils.py +54 -0
- oceanproc/grouplevel/dscalar_utils.py +51 -0
- oceanproc/grouplevel/run.py +18 -0
- oceanproc/grouplevel/utils.py +4 -0
- oceanproc/ocean_proc.py +215 -0
- oceanproc/oceanparse.py +107 -0
- oceanproc/utils.py +224 -0
- oceanproc/web/TODO.md +4 -0
- oceanproc/web/__init__.py +0 -0
- oceanproc/web/app.py +78 -0
- oceanproc/web/config.json +7 -0
- oceanproc/web/get_parser_json.py +58 -0
- oceanproc/web/static/components/ArgparseForm.js +9 -0
- oceanproc/web/static/components/ArgparseFormEntry.js +52 -0
- oceanproc/web/static/components/DirIcon.js +11 -0
- oceanproc/web/static/components/FileBrowser.js +85 -0
- oceanproc/web/static/components/FileBrowserOpenBtn.js +12 -0
- oceanproc/web/static/components/FileEntry.js +32 -0
- oceanproc/web/static/components/FileIcon.js +11 -0
- oceanproc/web/static/components/OceanprocForm.js +53 -0
- oceanproc/web/static/components/styles.css +44 -0
- oceanproc/web/templates/filesystem.html +13 -0
- oceanproc/web/templates/filesystem_fail.html +2 -0
- oceanproc/web/templates/oceanproc_form.html +23 -0
- oceanproc/web/templates/react-test.html +0 -0
- oceanproc/web/templates/username_card_component_test.html +39 -0
- oceanproc-0.1.0.dist-info/METADATA +29 -0
- oceanproc-0.1.0.dist-info/RECORD +41 -0
- oceanproc-0.1.0.dist-info/WHEEL +4 -0
- 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
|