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.
Files changed (30) hide show
  1. oceanfla-0.1.0/PKG-INFO +19 -0
  2. oceanfla-0.1.0/README.md +2 -0
  3. oceanfla-0.1.0/oceanfla/__init__.py +0 -0
  4. oceanfla-0.1.0/oceanfla/config.py +212 -0
  5. oceanfla-0.1.0/oceanfla/interfaces/__init__.py +0 -0
  6. oceanfla-0.1.0/oceanfla/interfaces/clean.py +237 -0
  7. oceanfla-0.1.0/oceanfla/interfaces/events.py +446 -0
  8. oceanfla-0.1.0/oceanfla/interfaces/exclusions.py +134 -0
  9. oceanfla-0.1.0/oceanfla/interfaces/nuisance.py +129 -0
  10. oceanfla-0.1.0/oceanfla/interfaces/regression.py +522 -0
  11. oceanfla-0.1.0/oceanfla/interfaces/reporting.py +79 -0
  12. oceanfla-0.1.0/oceanfla/interfaces/tests/__init__.py +0 -0
  13. oceanfla-0.1.0/oceanfla/interfaces/tests/conftest.py +77 -0
  14. oceanfla-0.1.0/oceanfla/interfaces/tests/data/confounds.tsv +336 -0
  15. oceanfla-0.1.0/oceanfla/interfaces/tests/test_filter.py +17 -0
  16. oceanfla-0.1.0/oceanfla/interfaces/tests/test_nuisance.py +13 -0
  17. oceanfla-0.1.0/oceanfla/interfaces/tests/test_tmask.py +50 -0
  18. oceanfla-0.1.0/oceanfla/interfaces/tmask.py +123 -0
  19. oceanfla-0.1.0/oceanfla/interfaces/utility.py +347 -0
  20. oceanfla-0.1.0/oceanfla/interfaces/workbench_utils.py +149 -0
  21. oceanfla-0.1.0/oceanfla/oceanparse.py +111 -0
  22. oceanfla-0.1.0/oceanfla/parser.py +429 -0
  23. oceanfla-0.1.0/oceanfla/resources/bids_paths.json +8 -0
  24. oceanfla-0.1.0/oceanfla/run.py +42 -0
  25. oceanfla-0.1.0/oceanfla/run_glm.py +1323 -0
  26. oceanfla-0.1.0/oceanfla/tests/__init__.py +0 -0
  27. oceanfla-0.1.0/oceanfla/tests/test_run_glm.py +170 -0
  28. oceanfla-0.1.0/oceanfla/utilities.py +455 -0
  29. oceanfla-0.1.0/oceanfla/workflows.py +1399 -0
  30. oceanfla-0.1.0/pyproject.toml +49 -0
@@ -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
@@ -0,0 +1,2 @@
1
+ # oceanfla
2
+ First level analysis package for Ocean Labs
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