oceanfla 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.
- oceanfla-0.1.0/PKG-INFO +19 -0
- oceanfla-0.1.0/README.md +2 -0
- oceanfla-0.1.0/oceanfla/__init__.py +0 -0
- oceanfla-0.1.0/oceanfla/config.py +212 -0
- oceanfla-0.1.0/oceanfla/interfaces/__init__.py +0 -0
- oceanfla-0.1.0/oceanfla/interfaces/clean.py +237 -0
- oceanfla-0.1.0/oceanfla/interfaces/events.py +446 -0
- oceanfla-0.1.0/oceanfla/interfaces/exclusions.py +134 -0
- oceanfla-0.1.0/oceanfla/interfaces/nuisance.py +129 -0
- oceanfla-0.1.0/oceanfla/interfaces/regression.py +522 -0
- oceanfla-0.1.0/oceanfla/interfaces/reporting.py +79 -0
- oceanfla-0.1.0/oceanfla/interfaces/tests/__init__.py +0 -0
- oceanfla-0.1.0/oceanfla/interfaces/tests/conftest.py +77 -0
- oceanfla-0.1.0/oceanfla/interfaces/tests/data/confounds.tsv +336 -0
- oceanfla-0.1.0/oceanfla/interfaces/tests/test_filter.py +17 -0
- oceanfla-0.1.0/oceanfla/interfaces/tests/test_nuisance.py +13 -0
- oceanfla-0.1.0/oceanfla/interfaces/tests/test_tmask.py +50 -0
- oceanfla-0.1.0/oceanfla/interfaces/tmask.py +123 -0
- oceanfla-0.1.0/oceanfla/interfaces/utility.py +347 -0
- oceanfla-0.1.0/oceanfla/interfaces/workbench_utils.py +149 -0
- oceanfla-0.1.0/oceanfla/oceanparse.py +111 -0
- oceanfla-0.1.0/oceanfla/parser.py +429 -0
- oceanfla-0.1.0/oceanfla/resources/bids_paths.json +8 -0
- oceanfla-0.1.0/oceanfla/run.py +42 -0
- oceanfla-0.1.0/oceanfla/run_glm.py +1323 -0
- oceanfla-0.1.0/oceanfla/tests/__init__.py +0 -0
- oceanfla-0.1.0/oceanfla/tests/test_run_glm.py +170 -0
- oceanfla-0.1.0/oceanfla/utilities.py +455 -0
- oceanfla-0.1.0/oceanfla/workflows.py +1399 -0
- oceanfla-0.1.0/pyproject.toml +49 -0
oceanfla-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: oceanfla
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: First level analysis for WUSTL Ocean labs
|
|
5
|
+
Author-Email: Ramone Agard <rhagard@wustl.edu>, Joey Scanga <joeyscanga92@gmail.com>
|
|
6
|
+
Requires-Python: <3.13,>=3.10
|
|
7
|
+
Requires-Dist: sphinx-rtd-theme<3.0.0,>=2.0.0
|
|
8
|
+
Requires-Dist: numpy>=1.26.0
|
|
9
|
+
Requires-Dist: pandas<3.0.0,>=2.2.2
|
|
10
|
+
Requires-Dist: nilearn<1.0.0,>=0.11.0
|
|
11
|
+
Requires-Dist: scipy>=1.13.0
|
|
12
|
+
Requires-Dist: matplotlib<4.0.0,>=3.9.1
|
|
13
|
+
Requires-Dist: pybids==0.16.4
|
|
14
|
+
Requires-Dist: nipype>=1.10.0
|
|
15
|
+
Requires-Dist: niworkflows>=1.14.3
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# oceanfla
|
|
19
|
+
First level analysis package for Ocean Labs
|
oceanfla-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import bids
|
|
3
|
+
import json
|
|
4
|
+
import multiprocessing
|
|
5
|
+
import logging
|
|
6
|
+
from logging.handlers import QueueHandler
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Options():
|
|
10
|
+
'''
|
|
11
|
+
A singleton class designed to be the holder of all user parsed arguments.
|
|
12
|
+
This class can only be initialized once, afterward, the same instance is returned.
|
|
13
|
+
'''
|
|
14
|
+
_instance = None
|
|
15
|
+
_initialized = False
|
|
16
|
+
layouts = []
|
|
17
|
+
logger_names = ["nipype.workflow", "nipype.utils", "nipype.interface"]
|
|
18
|
+
generic_nuisance_columns = ["mean", "trend", "spike"]
|
|
19
|
+
_pattern_file = Path(__file__).resolve().parent / "resources" / "bids_paths.json"
|
|
20
|
+
log_format = '%(asctime)s,%(msecs)d %(name)-2s %(levelname)-2s:\n\t %(message)s'
|
|
21
|
+
|
|
22
|
+
def __new__(cls, *args, **kwargs):
|
|
23
|
+
if cls._instance is None:
|
|
24
|
+
cls._instance = super().__new__(cls)
|
|
25
|
+
return cls._instance
|
|
26
|
+
|
|
27
|
+
def __init__(self, opts=None):
|
|
28
|
+
if not self._initialized and opts:
|
|
29
|
+
# set up the global options
|
|
30
|
+
option_msg_list = ["User Inputs:", "-"*30]
|
|
31
|
+
|
|
32
|
+
for k, v in opts.items():
|
|
33
|
+
# find the layouts
|
|
34
|
+
if isinstance(v, bids.BIDSLayout):
|
|
35
|
+
self.layouts.append(v)
|
|
36
|
+
# check for variable regroupings
|
|
37
|
+
elif k == "group" and v:
|
|
38
|
+
option_msg_list.append(f" {k} : {v}")
|
|
39
|
+
gmap = dict()
|
|
40
|
+
for regroup in v:
|
|
41
|
+
group_rename = regroup[-1]
|
|
42
|
+
for i in range(len(regroup)-1):
|
|
43
|
+
gmap[regroup[i]] = group_rename
|
|
44
|
+
setattr(self, k, gmap)
|
|
45
|
+
continue
|
|
46
|
+
else:
|
|
47
|
+
option_msg_list.append(f" {k} : {v}")
|
|
48
|
+
# add the option to the class attributes
|
|
49
|
+
setattr(self, k, v)
|
|
50
|
+
|
|
51
|
+
option_msg_list.append("-"*30)
|
|
52
|
+
|
|
53
|
+
# start the logging subprocess
|
|
54
|
+
log_process, log_queue = config_logging_process(self.log_file, self.log_level, self.log_format)
|
|
55
|
+
setattr(self, 'log_queue', log_queue)
|
|
56
|
+
setattr(self, 'log_process', log_process)
|
|
57
|
+
for log_name in self.logger_names:
|
|
58
|
+
logger = get_logger(log_name, log_queue)
|
|
59
|
+
logger.setLevel(self.log_level)
|
|
60
|
+
|
|
61
|
+
# log the arguments used for this run
|
|
62
|
+
logger = get_logger("nipype.utils")
|
|
63
|
+
logger.info("\n\t".join(option_msg_list))
|
|
64
|
+
|
|
65
|
+
self._initialized = True
|
|
66
|
+
self.bids_patterns = json.loads(self._pattern_file.read_text())["oceanfla_patterns"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
all_opts = Options()
|
|
70
|
+
|
|
71
|
+
def set_configs(args):
|
|
72
|
+
all_opts.__init__(args)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_layout_for_file(file) -> bids.BIDSLayout:
|
|
76
|
+
'''
|
|
77
|
+
Function to return the corresponding bids.BIDSLayout
|
|
78
|
+
for a given filepath
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
file: str
|
|
83
|
+
The path of the file belonging to some BIDSLayout / BIDS directory
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
bids.BIDSLayout
|
|
89
|
+
The BIDSLayout object if the file belongs to a parsed BIDS directory
|
|
90
|
+
|
|
91
|
+
'''
|
|
92
|
+
if isinstance(file, Path):
|
|
93
|
+
file = str(file.resolve())
|
|
94
|
+
if isinstance(file, str):
|
|
95
|
+
file = str(Path(file).resolve())
|
|
96
|
+
else:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"argument must be of type Path or str, not {type(file)}")
|
|
99
|
+
|
|
100
|
+
for lay in all_opts.layouts:
|
|
101
|
+
if file.startswith(str(lay._root.resolve())):
|
|
102
|
+
return lay
|
|
103
|
+
raise RuntimeError(f"No layout correspond to the input file {file}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_bids_file(file):
|
|
108
|
+
'''
|
|
109
|
+
Function to return the corresponding bids.layout.BIDSFile
|
|
110
|
+
for a given filepath
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
file: str
|
|
115
|
+
The path of the file to convert
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
bids.layout.BIDSFile
|
|
121
|
+
The BIDSFile object or None if it is not found
|
|
122
|
+
|
|
123
|
+
'''
|
|
124
|
+
if isinstance(file, bids.layout.BIDSFile):
|
|
125
|
+
return file
|
|
126
|
+
file_layout = get_layout_for_file(file)
|
|
127
|
+
return file_layout.get_file(file)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def file_log_process(q, log_file, log_level=logging.INFO, log_fmt=None):
|
|
131
|
+
|
|
132
|
+
# remove current handlers
|
|
133
|
+
for logger_name in logging.Logger.manager.loggerDict:
|
|
134
|
+
logger = logging.getLogger(logger_name)
|
|
135
|
+
if isinstance(logger, logging.Logger):
|
|
136
|
+
for handler in logger.handlers[:]:
|
|
137
|
+
logger.removeHandler(handler)
|
|
138
|
+
handler.close()
|
|
139
|
+
# Special handling for the root logger, which is not in loggerDict but is accessible
|
|
140
|
+
root = logging.getLogger()
|
|
141
|
+
for handler in root.handlers[:]:
|
|
142
|
+
root.removeHandler(handler)
|
|
143
|
+
handler.close()
|
|
144
|
+
|
|
145
|
+
file_handler = logging.FileHandler(log_file)
|
|
146
|
+
fmtr = logging.Formatter(log_fmt)
|
|
147
|
+
file_handler.setFormatter(fmtr)
|
|
148
|
+
root.addHandler(file_handler)
|
|
149
|
+
root.setLevel(log_level)
|
|
150
|
+
|
|
151
|
+
while True:
|
|
152
|
+
try:
|
|
153
|
+
record = q.get()
|
|
154
|
+
if record is None:
|
|
155
|
+
break
|
|
156
|
+
logger = logging.getLogger(record.name)
|
|
157
|
+
logger.handle(record)
|
|
158
|
+
except Exception:
|
|
159
|
+
msg = "Error in the file logging process"
|
|
160
|
+
root.error(msg)
|
|
161
|
+
print(msg)
|
|
162
|
+
break
|
|
163
|
+
root.info("closing log file")
|
|
164
|
+
file_handler.close()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def config_logging_process(log_file, log_level=logging.INFO, log_fmt=None):
|
|
168
|
+
log_q = multiprocessing.Queue(-1)
|
|
169
|
+
|
|
170
|
+
log_process = multiprocessing.Process(target=file_log_process, args=(log_q, log_file, log_level, log_fmt))
|
|
171
|
+
log_process.start()
|
|
172
|
+
|
|
173
|
+
logger = get_logger("nipype.utils", log_q)
|
|
174
|
+
logger.info("File logging started")
|
|
175
|
+
|
|
176
|
+
return log_process, log_q
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def finish_logging():
|
|
180
|
+
try:
|
|
181
|
+
logger = get_logger('nipype.utils')
|
|
182
|
+
logger.info("Ending log")
|
|
183
|
+
except:
|
|
184
|
+
print("Ending log")
|
|
185
|
+
finally:
|
|
186
|
+
if all_opts.log_process:
|
|
187
|
+
all_opts.log_queue.put_nowait(None)
|
|
188
|
+
all_opts.log_process.join()
|
|
189
|
+
logging.shutdown()
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_logger(name, q=None):
|
|
194
|
+
if q is None:
|
|
195
|
+
q = all_opts.log_queue
|
|
196
|
+
|
|
197
|
+
logger = logging.getLogger(name)
|
|
198
|
+
has_queue_hdlr = any([isinstance(hdlr, QueueHandler)
|
|
199
|
+
for hdlr in logger.handlers])
|
|
200
|
+
if not has_queue_hdlr:
|
|
201
|
+
queue_handler = QueueHandler(q)
|
|
202
|
+
logger.addHandler(queue_handler)
|
|
203
|
+
|
|
204
|
+
return logger
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def close_layouts():
|
|
208
|
+
for lay in all_opts.layouts:
|
|
209
|
+
lay.connection_manager.session.close()
|
|
210
|
+
del all_opts.layouts[:]
|
|
211
|
+
|
|
212
|
+
|
|
File without changes
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from nipype.interfaces.base import (
|
|
2
|
+
File,
|
|
3
|
+
traits,
|
|
4
|
+
)
|
|
5
|
+
from oceanfla.interfaces.utility import (
|
|
6
|
+
OptionalInterface,
|
|
7
|
+
OptionalInterfaceSpec,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
class _FilterDataInputSpec(OptionalInterfaceSpec):
|
|
11
|
+
bold_in = File(
|
|
12
|
+
exists=True, mandatory=True,
|
|
13
|
+
desc="Path to unfiltered timeseries (as a .nii, .nii.gz, or .dtseries.nii)."
|
|
14
|
+
)
|
|
15
|
+
tmask_in = File(
|
|
16
|
+
exists=True, mandatory=True,
|
|
17
|
+
desc="Run mask (as a .txt)."
|
|
18
|
+
)
|
|
19
|
+
high_pass = traits.Float(
|
|
20
|
+
default_value=0.008,
|
|
21
|
+
desc="The lowest frequency allowed (Hz)"
|
|
22
|
+
)
|
|
23
|
+
low_pass = traits.Float(
|
|
24
|
+
default_value=0.1,
|
|
25
|
+
desc="The highest frequency allowed (Hz)"
|
|
26
|
+
)
|
|
27
|
+
tr = traits.Float(
|
|
28
|
+
desc="The Repetition Time for this BOLD run"
|
|
29
|
+
)
|
|
30
|
+
padtype = traits.Str(
|
|
31
|
+
"mean",
|
|
32
|
+
desc="Type of padding to use -- choices: 'odd', 'even', 'constant', 'zero', or 'none'"
|
|
33
|
+
)
|
|
34
|
+
padlen = traits.Int(
|
|
35
|
+
50,
|
|
36
|
+
desc="Length of pad."
|
|
37
|
+
)
|
|
38
|
+
brain_mask = traits.Union(
|
|
39
|
+
traits.File(exists=True),
|
|
40
|
+
None,
|
|
41
|
+
default_value=None,
|
|
42
|
+
desc="The brain mask that accompanies volumetric data"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _FilterDataOutputSpec(OptionalInterfaceSpec):
|
|
47
|
+
bold_file = File(
|
|
48
|
+
exists=True,
|
|
49
|
+
desc="Filtered timeseries."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class FilterData(OptionalInterface):
|
|
54
|
+
"""
|
|
55
|
+
Generates a nuisance matrix for regression before final GLM.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
input_spec = _FilterDataInputSpec
|
|
59
|
+
output_spec = _FilterDataOutputSpec
|
|
60
|
+
|
|
61
|
+
def _run_interface(self, runtime):
|
|
62
|
+
|
|
63
|
+
self._results["bold_file"] = filter_data(
|
|
64
|
+
func_file=self.inputs.bold_in,
|
|
65
|
+
tmask_file=self.inputs.tmask_in,
|
|
66
|
+
tr=self.inputs.tr,
|
|
67
|
+
low_pass=self.inputs.low_pass,
|
|
68
|
+
high_pass=self.inputs.high_pass,
|
|
69
|
+
padtype=self.inputs.padtype,
|
|
70
|
+
padlen=self.inputs.padlen,
|
|
71
|
+
brain_mask=self.inputs.brain_mask
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return runtime
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class PercentChangeInputSpec(OptionalInterfaceSpec):
|
|
79
|
+
bold_in = File(exists=True, mandatory=True,
|
|
80
|
+
desc="A BIDS style bold file")
|
|
81
|
+
|
|
82
|
+
tmask_in = File(
|
|
83
|
+
exists=True, mandatory=True,
|
|
84
|
+
desc="Run mask (as a .txt)."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
brain_mask = traits.Union(
|
|
88
|
+
traits.File(exists=True),
|
|
89
|
+
None,
|
|
90
|
+
default_value=None,
|
|
91
|
+
desc="The brain mask that accompanies volumetric data"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class PercentChangeOutputSpec(OptionalInterfaceSpec):
|
|
96
|
+
bold_file = File(exists=True,
|
|
97
|
+
desc="The functional data after a percent signal change transformation")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class PercentChange(OptionalInterface):
|
|
101
|
+
input_spec = PercentChangeInputSpec
|
|
102
|
+
output_spec = PercentChangeOutputSpec
|
|
103
|
+
|
|
104
|
+
def _run_interface(self, runtime):
|
|
105
|
+
|
|
106
|
+
self._results["bold_file"] = percent_signal_change(
|
|
107
|
+
func_file=self.inputs.bold_in,
|
|
108
|
+
tmask_file=self.inputs.tmask_in,
|
|
109
|
+
brain_mask=self.inputs.brain_mask
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return runtime
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def filter_data(func_file: str,
|
|
117
|
+
tmask_file: str,
|
|
118
|
+
tr: float,
|
|
119
|
+
low_pass: float = 0.1,
|
|
120
|
+
high_pass: float = 0.008,
|
|
121
|
+
padtype: str = "mean",
|
|
122
|
+
padlen: int = 50,
|
|
123
|
+
brain_mask: str = None):
|
|
124
|
+
'''
|
|
125
|
+
Runs a butterworth filter on the data in the input file, using the filter
|
|
126
|
+
parameters specified. The temporal mask file is used to first censor the input data
|
|
127
|
+
before interpolating the missing frames, then running the filter.
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
#TODO
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
#TODO
|
|
136
|
+
'''
|
|
137
|
+
from nilearn.signal import butterworth, _handle_scrubbed_volumes
|
|
138
|
+
from oceanfla.utilities import load_data, create_image_like, replace_entities
|
|
139
|
+
import numpy as np
|
|
140
|
+
|
|
141
|
+
mask = np.loadtxt(tmask_file).astype(bool)
|
|
142
|
+
func_data = load_data(func_file, brain_mask)
|
|
143
|
+
|
|
144
|
+
if not any((
|
|
145
|
+
padtype == "none",
|
|
146
|
+
padlen is None,
|
|
147
|
+
(padtype != "zero" and padlen is not None and padlen > 0),
|
|
148
|
+
(padtype == "zero" and padlen is not None and padlen >= 2),
|
|
149
|
+
)):
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"Pad length of {padlen} incompatible with pad type {'odd' if padtype is None else padtype}")
|
|
152
|
+
|
|
153
|
+
padded_func_data = func_data
|
|
154
|
+
if padtype == "even":
|
|
155
|
+
padded_func_data = np.pad(
|
|
156
|
+
func_data, ((padlen, padlen), (0, 0)), mode='reflect')
|
|
157
|
+
elif padtype == "odd":
|
|
158
|
+
padded_func_data = np.pad(
|
|
159
|
+
func_data, ((padlen, padlen), (0, 0)), mode='reflect', reflect_type="odd")
|
|
160
|
+
elif padtype == "mean":
|
|
161
|
+
masked_mean = np.nanmean(func_data[mask], axis=0)
|
|
162
|
+
pad_arr = np.full(
|
|
163
|
+
shape=(padlen, func_data.shape[1]), fill_value=masked_mean)
|
|
164
|
+
padded_func_data = np.concatenate(
|
|
165
|
+
[pad_arr.copy(), func_data, pad_arr], axis=0)
|
|
166
|
+
elif padtype == "zero":
|
|
167
|
+
padded_func_data = np.pad(
|
|
168
|
+
func_data, ((padlen, padlen), (0, 0)), mode='constant', constant_values=0)
|
|
169
|
+
elif padtype == "edge":
|
|
170
|
+
padded_func_data = np.pad(
|
|
171
|
+
func_data, ((padlen, padlen), (0, 0)), mode='edge')
|
|
172
|
+
|
|
173
|
+
# padded_func_data = np.pad(func_data, ((padlen, padlen), (0, 0)), mode='mean')
|
|
174
|
+
padded_mask = np.pad(mask, (padlen, padlen), mode='constant',
|
|
175
|
+
constant_values=True) if padtype != "none" else mask
|
|
176
|
+
|
|
177
|
+
# if the mask is excluding frames, interpolate the censored frames
|
|
178
|
+
if np.sum(mask) < mask.shape[0]:
|
|
179
|
+
padded_func_data, _, padded_mask = _handle_scrubbed_volumes(
|
|
180
|
+
signals=padded_func_data,
|
|
181
|
+
confounds=None,
|
|
182
|
+
sample_mask=padded_mask,
|
|
183
|
+
filter_type="butterworth",
|
|
184
|
+
t_r=tr,
|
|
185
|
+
extrapolate=True
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
filtered_data = butterworth(
|
|
189
|
+
signals=padded_func_data,
|
|
190
|
+
sampling_rate=1.0 / tr,
|
|
191
|
+
low_pass=low_pass,
|
|
192
|
+
high_pass=high_pass,
|
|
193
|
+
padtype=None # if padtype == "none" else padtype,
|
|
194
|
+
)[padlen:-padlen, :] # remove 0-pad frames on both sides
|
|
195
|
+
|
|
196
|
+
assert filtered_data.shape[0] == func_data.shape[0], "Filtered data must have the same number of timepoints as the original functional data"
|
|
197
|
+
|
|
198
|
+
# save data out
|
|
199
|
+
out_path = replace_entities(
|
|
200
|
+
file=func_file,
|
|
201
|
+
entities={"suffix": "filtered-bold", "path": None}
|
|
202
|
+
)
|
|
203
|
+
create_image_like(data=filtered_data,
|
|
204
|
+
source_header=func_file,
|
|
205
|
+
out_file=out_path,
|
|
206
|
+
brain_mask=brain_mask)
|
|
207
|
+
return out_path
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def percent_signal_change(func_file: str,
|
|
211
|
+
tmask_file: str,
|
|
212
|
+
brain_mask: str = None):
|
|
213
|
+
from oceanfla.utilities import load_data, create_image_like, replace_entities
|
|
214
|
+
import numpy as np
|
|
215
|
+
|
|
216
|
+
mask = np.loadtxt(tmask_file).astype(bool)
|
|
217
|
+
data = load_data(func_file, brain_mask)
|
|
218
|
+
|
|
219
|
+
masked_data = data[mask, :]
|
|
220
|
+
mean = np.nanmean(masked_data, axis=0)
|
|
221
|
+
mean = np.repeat(mean[np.newaxis, :], data.shape[0], axis=0)
|
|
222
|
+
psc_data = ((data - mean) / np.abs(mean)) * 100
|
|
223
|
+
non_valid_indices = np.where(~np.isfinite(psc_data))
|
|
224
|
+
if len(non_valid_indices[0]) > 0:
|
|
225
|
+
# logger.warning("Found vertices with zero signal, setting these to zero")
|
|
226
|
+
psc_data[np.where(~np.isfinite(psc_data))] = 0
|
|
227
|
+
|
|
228
|
+
out_path = replace_entities(
|
|
229
|
+
file=func_file,
|
|
230
|
+
entities={"suffix": "percent-change-bold", "path": None}
|
|
231
|
+
)
|
|
232
|
+
create_image_like(data=psc_data,
|
|
233
|
+
source_header=func_file,
|
|
234
|
+
out_file=out_path,
|
|
235
|
+
brain_mask=brain_mask)
|
|
236
|
+
|
|
237
|
+
return out_path
|