junifer 0.0.3.dev188__py3-none-any.whl → 0.0.4__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 (178) hide show
  1. junifer/_version.py +14 -2
  2. junifer/api/cli.py +162 -17
  3. junifer/api/functions.py +87 -419
  4. junifer/api/parser.py +24 -0
  5. junifer/api/queue_context/__init__.py +8 -0
  6. junifer/api/queue_context/gnu_parallel_local_adapter.py +258 -0
  7. junifer/api/queue_context/htcondor_adapter.py +365 -0
  8. junifer/api/queue_context/queue_context_adapter.py +60 -0
  9. junifer/api/queue_context/tests/test_gnu_parallel_local_adapter.py +192 -0
  10. junifer/api/queue_context/tests/test_htcondor_adapter.py +257 -0
  11. junifer/api/res/afni/run_afni_docker.sh +6 -6
  12. junifer/api/res/ants/ResampleImage +3 -0
  13. junifer/api/res/ants/antsApplyTransforms +3 -0
  14. junifer/api/res/ants/antsApplyTransformsToPoints +3 -0
  15. junifer/api/res/ants/run_ants_docker.sh +39 -0
  16. junifer/api/res/fsl/applywarp +3 -0
  17. junifer/api/res/fsl/flirt +3 -0
  18. junifer/api/res/fsl/img2imgcoord +3 -0
  19. junifer/api/res/fsl/run_fsl_docker.sh +39 -0
  20. junifer/api/res/fsl/std2imgcoord +3 -0
  21. junifer/api/res/run_conda.sh +4 -4
  22. junifer/api/res/run_venv.sh +22 -0
  23. junifer/api/tests/data/partly_cloudy_agg_mean_tian.yml +16 -0
  24. junifer/api/tests/test_api_utils.py +21 -3
  25. junifer/api/tests/test_cli.py +232 -9
  26. junifer/api/tests/test_functions.py +211 -439
  27. junifer/api/tests/test_parser.py +1 -1
  28. junifer/configs/juseless/datagrabbers/aomic_id1000_vbm.py +6 -1
  29. junifer/configs/juseless/datagrabbers/camcan_vbm.py +6 -1
  30. junifer/configs/juseless/datagrabbers/ixi_vbm.py +6 -1
  31. junifer/configs/juseless/datagrabbers/tests/test_ucla.py +8 -8
  32. junifer/configs/juseless/datagrabbers/ucla.py +44 -26
  33. junifer/configs/juseless/datagrabbers/ukb_vbm.py +6 -1
  34. junifer/data/VOIs/meta/AutobiographicalMemory_VOIs.txt +23 -0
  35. junifer/data/VOIs/meta/Power2013_MNI_VOIs.tsv +264 -0
  36. junifer/data/__init__.py +4 -0
  37. junifer/data/coordinates.py +298 -31
  38. junifer/data/masks.py +360 -28
  39. junifer/data/parcellations.py +621 -188
  40. junifer/data/template_spaces.py +190 -0
  41. junifer/data/tests/test_coordinates.py +34 -3
  42. junifer/data/tests/test_data_utils.py +1 -0
  43. junifer/data/tests/test_masks.py +202 -86
  44. junifer/data/tests/test_parcellations.py +266 -55
  45. junifer/data/tests/test_template_spaces.py +104 -0
  46. junifer/data/utils.py +4 -2
  47. junifer/datagrabber/__init__.py +1 -0
  48. junifer/datagrabber/aomic/id1000.py +111 -70
  49. junifer/datagrabber/aomic/piop1.py +116 -53
  50. junifer/datagrabber/aomic/piop2.py +116 -53
  51. junifer/datagrabber/aomic/tests/test_id1000.py +27 -27
  52. junifer/datagrabber/aomic/tests/test_piop1.py +27 -27
  53. junifer/datagrabber/aomic/tests/test_piop2.py +27 -27
  54. junifer/datagrabber/base.py +62 -10
  55. junifer/datagrabber/datalad_base.py +0 -2
  56. junifer/datagrabber/dmcc13_benchmark.py +372 -0
  57. junifer/datagrabber/hcp1200/datalad_hcp1200.py +5 -0
  58. junifer/datagrabber/hcp1200/hcp1200.py +30 -13
  59. junifer/datagrabber/pattern.py +133 -27
  60. junifer/datagrabber/pattern_datalad.py +111 -13
  61. junifer/datagrabber/tests/test_base.py +57 -6
  62. junifer/datagrabber/tests/test_datagrabber_utils.py +204 -76
  63. junifer/datagrabber/tests/test_datalad_base.py +0 -6
  64. junifer/datagrabber/tests/test_dmcc13_benchmark.py +256 -0
  65. junifer/datagrabber/tests/test_multiple.py +43 -10
  66. junifer/datagrabber/tests/test_pattern.py +125 -178
  67. junifer/datagrabber/tests/test_pattern_datalad.py +44 -25
  68. junifer/datagrabber/utils.py +151 -16
  69. junifer/datareader/default.py +36 -10
  70. junifer/external/nilearn/junifer_nifti_spheres_masker.py +6 -0
  71. junifer/markers/base.py +25 -16
  72. junifer/markers/collection.py +35 -16
  73. junifer/markers/complexity/__init__.py +27 -0
  74. junifer/markers/complexity/complexity_base.py +149 -0
  75. junifer/markers/complexity/hurst_exponent.py +136 -0
  76. junifer/markers/complexity/multiscale_entropy_auc.py +140 -0
  77. junifer/markers/complexity/perm_entropy.py +132 -0
  78. junifer/markers/complexity/range_entropy.py +136 -0
  79. junifer/markers/complexity/range_entropy_auc.py +145 -0
  80. junifer/markers/complexity/sample_entropy.py +134 -0
  81. junifer/markers/complexity/tests/test_complexity_base.py +19 -0
  82. junifer/markers/complexity/tests/test_hurst_exponent.py +69 -0
  83. junifer/markers/complexity/tests/test_multiscale_entropy_auc.py +68 -0
  84. junifer/markers/complexity/tests/test_perm_entropy.py +68 -0
  85. junifer/markers/complexity/tests/test_range_entropy.py +69 -0
  86. junifer/markers/complexity/tests/test_range_entropy_auc.py +69 -0
  87. junifer/markers/complexity/tests/test_sample_entropy.py +68 -0
  88. junifer/markers/complexity/tests/test_weighted_perm_entropy.py +68 -0
  89. junifer/markers/complexity/weighted_perm_entropy.py +133 -0
  90. junifer/markers/falff/_afni_falff.py +153 -0
  91. junifer/markers/falff/_junifer_falff.py +142 -0
  92. junifer/markers/falff/falff_base.py +91 -84
  93. junifer/markers/falff/falff_parcels.py +61 -45
  94. junifer/markers/falff/falff_spheres.py +64 -48
  95. junifer/markers/falff/tests/test_falff_parcels.py +89 -121
  96. junifer/markers/falff/tests/test_falff_spheres.py +92 -127
  97. junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py +1 -0
  98. junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py +1 -0
  99. junifer/markers/functional_connectivity/functional_connectivity_base.py +1 -0
  100. junifer/markers/functional_connectivity/tests/test_crossparcellation_functional_connectivity.py +46 -44
  101. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_parcels.py +34 -39
  102. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_spheres.py +40 -52
  103. junifer/markers/functional_connectivity/tests/test_functional_connectivity_parcels.py +62 -70
  104. junifer/markers/functional_connectivity/tests/test_functional_connectivity_spheres.py +99 -85
  105. junifer/markers/parcel_aggregation.py +60 -38
  106. junifer/markers/reho/_afni_reho.py +192 -0
  107. junifer/markers/reho/_junifer_reho.py +281 -0
  108. junifer/markers/reho/reho_base.py +69 -34
  109. junifer/markers/reho/reho_parcels.py +26 -16
  110. junifer/markers/reho/reho_spheres.py +23 -9
  111. junifer/markers/reho/tests/test_reho_parcels.py +93 -92
  112. junifer/markers/reho/tests/test_reho_spheres.py +88 -86
  113. junifer/markers/sphere_aggregation.py +54 -9
  114. junifer/markers/temporal_snr/temporal_snr_base.py +1 -0
  115. junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py +38 -37
  116. junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py +34 -38
  117. junifer/markers/tests/test_collection.py +43 -42
  118. junifer/markers/tests/test_ets_rss.py +29 -37
  119. junifer/markers/tests/test_parcel_aggregation.py +587 -468
  120. junifer/markers/tests/test_sphere_aggregation.py +209 -157
  121. junifer/markers/utils.py +2 -40
  122. junifer/onthefly/read_transform.py +13 -6
  123. junifer/pipeline/__init__.py +1 -0
  124. junifer/pipeline/pipeline_step_mixin.py +105 -41
  125. junifer/pipeline/registry.py +17 -0
  126. junifer/pipeline/singleton.py +45 -0
  127. junifer/pipeline/tests/test_pipeline_step_mixin.py +139 -51
  128. junifer/pipeline/tests/test_update_meta_mixin.py +1 -0
  129. junifer/pipeline/tests/test_workdir_manager.py +104 -0
  130. junifer/pipeline/update_meta_mixin.py +8 -2
  131. junifer/pipeline/utils.py +154 -15
  132. junifer/pipeline/workdir_manager.py +246 -0
  133. junifer/preprocess/__init__.py +3 -0
  134. junifer/preprocess/ants/__init__.py +4 -0
  135. junifer/preprocess/ants/ants_apply_transforms_warper.py +185 -0
  136. junifer/preprocess/ants/tests/test_ants_apply_transforms_warper.py +56 -0
  137. junifer/preprocess/base.py +96 -69
  138. junifer/preprocess/bold_warper.py +265 -0
  139. junifer/preprocess/confounds/fmriprep_confound_remover.py +91 -134
  140. junifer/preprocess/confounds/tests/test_fmriprep_confound_remover.py +106 -111
  141. junifer/preprocess/fsl/__init__.py +4 -0
  142. junifer/preprocess/fsl/apply_warper.py +179 -0
  143. junifer/preprocess/fsl/tests/test_apply_warper.py +45 -0
  144. junifer/preprocess/tests/test_bold_warper.py +159 -0
  145. junifer/preprocess/tests/test_preprocess_base.py +6 -6
  146. junifer/preprocess/warping/__init__.py +6 -0
  147. junifer/preprocess/warping/_ants_warper.py +167 -0
  148. junifer/preprocess/warping/_fsl_warper.py +109 -0
  149. junifer/preprocess/warping/space_warper.py +213 -0
  150. junifer/preprocess/warping/tests/test_space_warper.py +198 -0
  151. junifer/stats.py +18 -4
  152. junifer/storage/base.py +9 -1
  153. junifer/storage/hdf5.py +8 -3
  154. junifer/storage/pandas_base.py +2 -1
  155. junifer/storage/sqlite.py +1 -0
  156. junifer/storage/tests/test_hdf5.py +2 -1
  157. junifer/storage/tests/test_sqlite.py +8 -8
  158. junifer/storage/tests/test_utils.py +6 -6
  159. junifer/storage/utils.py +1 -0
  160. junifer/testing/datagrabbers.py +11 -7
  161. junifer/testing/utils.py +1 -0
  162. junifer/tests/test_stats.py +2 -0
  163. junifer/utils/__init__.py +1 -0
  164. junifer/utils/helpers.py +53 -0
  165. junifer/utils/logging.py +14 -3
  166. junifer/utils/tests/test_helpers.py +35 -0
  167. {junifer-0.0.3.dev188.dist-info → junifer-0.0.4.dist-info}/METADATA +59 -28
  168. junifer-0.0.4.dist-info/RECORD +257 -0
  169. {junifer-0.0.3.dev188.dist-info → junifer-0.0.4.dist-info}/WHEEL +1 -1
  170. junifer/markers/falff/falff_estimator.py +0 -334
  171. junifer/markers/falff/tests/test_falff_estimator.py +0 -238
  172. junifer/markers/reho/reho_estimator.py +0 -515
  173. junifer/markers/reho/tests/test_reho_estimator.py +0 -260
  174. junifer-0.0.3.dev188.dist-info/RECORD +0 -199
  175. {junifer-0.0.3.dev188.dist-info → junifer-0.0.4.dist-info}/AUTHORS.rst +0 -0
  176. {junifer-0.0.3.dev188.dist-info → junifer-0.0.4.dist-info}/LICENSE.md +0 -0
  177. {junifer-0.0.3.dev188.dist-info → junifer-0.0.4.dist-info}/entry_points.txt +0 -0
  178. {junifer-0.0.3.dev188.dist-info → junifer-0.0.4.dist-info}/top_level.txt +0 -0
@@ -23,19 +23,25 @@ class UpdateMetaMixin:
23
23
  The data object to update.
24
24
  step_name : str
25
25
  The name of the pipeline step.
26
+
26
27
  """
28
+ # Initialize empty dictionary for the step's metadata
27
29
  t_meta = {}
30
+ # Set class name for the step
28
31
  t_meta["class"] = self.__class__.__name__
32
+ # Add object variables to metadata if name doesn't start with "_"
29
33
  for k, v in vars(self).items():
30
34
  if not k.startswith("_"):
31
35
  t_meta[k] = v
32
-
36
+ # Add "meta" to the step's local context dict
33
37
  if "meta" not in input:
34
38
  input["meta"] = {}
39
+ # Add step name
35
40
  input["meta"][step_name] = t_meta
41
+ # Add step dependencies
36
42
  if "dependencies" not in input["meta"]:
37
43
  input["meta"]["dependencies"] = set()
38
-
44
+ # Update step dependencies
39
45
  dependencies = getattr(self, "_DEPENDENCIES", set())
40
46
  if dependencies is not None:
41
47
  if not isinstance(dependencies, (set, list)):
junifer/pipeline/utils.py CHANGED
@@ -10,16 +10,17 @@ from typing import Any, List, Optional
10
10
  from junifer.utils.logging import raise_error, warn_with_log
11
11
 
12
12
 
13
- def check_ext_dependencies(name: str, optional: bool, **kwargs: Any) -> bool:
13
+ def check_ext_dependencies(
14
+ name: str, optional: bool = False, **kwargs: Any
15
+ ) -> bool:
14
16
  """Check if external dependency `name` is found if mandatory.
15
17
 
16
18
  Parameters
17
19
  ----------
18
20
  name : str
19
21
  The name of the dependency.
20
- optional : bool
21
- Whether the dependency is optional. For external dependencies marked
22
- as optional, there should be an implementation provided with junfier.
22
+ optional : bool, optional
23
+ Whether the dependency is optional (default False).
23
24
  **kwargs : dict
24
25
  Extra keyword arguments.
25
26
 
@@ -28,33 +29,49 @@ def check_ext_dependencies(name: str, optional: bool, **kwargs: Any) -> bool:
28
29
  bool
29
30
  Whether the external dependency was found.
30
31
 
32
+ Raises
33
+ ------
34
+ ValueError
35
+ If ``name`` is invalid.
36
+ RuntimeError
37
+ If ``name`` is mandatory and is not found.
38
+
31
39
  """
40
+ valid_ext_dependencies = ("afni", "fsl", "ants")
41
+ if name not in valid_ext_dependencies:
42
+ raise_error(
43
+ "Invalid value for `name`, should be one of: "
44
+ f"{valid_ext_dependencies}"
45
+ )
32
46
  # Check for afni
33
47
  if name == "afni":
34
48
  found = _check_afni(**kwargs)
35
- # Went off the rails
36
- else:
37
- raise_error(
38
- f"The external dependency {name} has no check. "
39
- f"Either the name '{name}' is incorrect or you were too "
40
- "adventurous. Raise an issue if it's the latter ;-)."
41
- )
49
+ # Check for fsl
50
+ elif name == "fsl":
51
+ found = _check_fsl(**kwargs)
52
+ # Check for ants
53
+ elif name == "ants":
54
+ found = _check_ants(**kwargs)
55
+
42
56
  # Check if the dependency is mandatory in case it's not found
43
57
  if not found and not optional:
44
58
  raise_error(
45
- f"{name} is not installed but is "
46
- "required by one of the pipeline steps."
59
+ msg=(
60
+ f"{name} is not installed but is "
61
+ "required by one of the pipeline steps"
62
+ ),
63
+ klass=RuntimeError,
47
64
  )
48
65
  return found
49
66
 
50
67
 
51
68
  def _check_afni(commands: Optional[List[str]] = None) -> bool:
52
- """Check if afni is present in the system.
69
+ """Check if AFNI is present in the system.
53
70
 
54
71
  Parameters
55
72
  ----------
56
73
  commands : list of str, optional
57
- The commands to specifically check for from afni. If None, only
74
+ The commands to specifically check for from AFNI. If None, only
58
75
  the basic afni version would be looked up, else, would also
59
76
  check for specific commands (default None).
60
77
 
@@ -106,3 +123,125 @@ def _check_afni(commands: Optional[List[str]] = None) -> bool:
106
123
  f"{commands_found_results}"
107
124
  )
108
125
  return afni_found
126
+
127
+
128
+ def _check_fsl(commands: Optional[List[str]] = None) -> bool:
129
+ """Check if FSL is present in the system.
130
+
131
+ Parameters
132
+ ----------
133
+ commands : list of str, optional
134
+ The commands to specifically check for from FSL. If None, only
135
+ the basic FSL flirt version would be looked up, else, would also
136
+ check for specific commands (default None).
137
+
138
+ Returns
139
+ -------
140
+ bool
141
+ Whether FSL is found or not.
142
+
143
+ """
144
+ completed_process = subprocess.run(
145
+ "flirt",
146
+ stdin=subprocess.DEVNULL,
147
+ stdout=subprocess.DEVNULL,
148
+ stderr=subprocess.STDOUT,
149
+ shell=True, # is unsafe but kept for resolution via PATH
150
+ check=False,
151
+ )
152
+ fsl_found = completed_process.returncode == 1
153
+
154
+ # Check for specific commands
155
+ if fsl_found and commands is not None:
156
+ if not isinstance(commands, list):
157
+ commands = [commands]
158
+ # Store command found results
159
+ commands_found_results = {}
160
+ # Set all commands found flag to True
161
+ all_commands_found = True
162
+ # Check commands' existence
163
+ for command in commands:
164
+ command_process = subprocess.run(
165
+ [command],
166
+ stdin=subprocess.DEVNULL,
167
+ stdout=subprocess.DEVNULL,
168
+ stderr=subprocess.STDOUT,
169
+ shell=True, # is unsafe but kept for resolution via PATH
170
+ check=False,
171
+ )
172
+ # FSL commands are incoherent with respect to status code hence a
173
+ # blanket to only look for no command found
174
+ command_found = command_process.returncode != 127
175
+ commands_found_results[command] = (
176
+ "found" if command_found else "not found"
177
+ )
178
+ # Set flag to trigger warning
179
+ all_commands_found = all_commands_found and command_found
180
+ # One or more commands were missing
181
+ if not all_commands_found:
182
+ warn_with_log(
183
+ "FSL is installed but some of the required commands "
184
+ "were not found. These are the results: "
185
+ f"{commands_found_results}"
186
+ )
187
+ return fsl_found
188
+
189
+
190
+ def _check_ants(commands: Optional[List[str]] = None) -> bool:
191
+ """Check if ANTs is present in the system.
192
+
193
+ Parameters
194
+ ----------
195
+ commands : list of str, optional
196
+ The commands to specifically check for from ANTs. If None, only
197
+ the basic ANTS help would be looked up, else, would also
198
+ check for specific commands (default None).
199
+
200
+ Returns
201
+ -------
202
+ bool
203
+ Whether ANTs is found or not.
204
+
205
+ """
206
+ completed_process = subprocess.run(
207
+ "ANTS --help",
208
+ stdin=subprocess.DEVNULL,
209
+ stdout=subprocess.DEVNULL,
210
+ stderr=subprocess.STDOUT,
211
+ shell=True, # is unsafe but kept for resolution via PATH
212
+ check=False,
213
+ )
214
+ ants_found = completed_process.returncode == 0
215
+
216
+ # Check for specific commands
217
+ if ants_found and commands is not None:
218
+ if not isinstance(commands, list):
219
+ commands = [commands]
220
+ # Store command found results
221
+ commands_found_results = {}
222
+ # Set all commands found flag to True
223
+ all_commands_found = True
224
+ # Check commands' existence
225
+ for command in commands:
226
+ command_process = subprocess.run(
227
+ [command],
228
+ stdin=subprocess.DEVNULL,
229
+ stdout=subprocess.DEVNULL,
230
+ stderr=subprocess.STDOUT,
231
+ shell=True, # is unsafe but kept for resolution via PATH
232
+ check=False,
233
+ )
234
+ command_found = command_process.returncode == 0
235
+ commands_found_results[command] = (
236
+ "found" if command_found else "not found"
237
+ )
238
+ # Set flag to trigger warning
239
+ all_commands_found = all_commands_found and command_found
240
+ # One or more commands were missing
241
+ if not all_commands_found:
242
+ warn_with_log(
243
+ "ANTs is installed but some of the required commands "
244
+ "were not found. These are the results: "
245
+ f"{commands_found_results}"
246
+ )
247
+ return ants_found
@@ -0,0 +1,246 @@
1
+ """Provide a work directory manager class to be used by pipeline components."""
2
+
3
+ # Authors: Synchon Mandal <s.mandal@fz-juelich.de>
4
+ # Federico Raimondo <f.raimondo@fz-juelich.de>
5
+ # License: AGPL
6
+
7
+ import shutil
8
+ import tempfile
9
+ from pathlib import Path
10
+ from typing import Optional, Union
11
+
12
+ from ..utils import logger
13
+ from .singleton import singleton
14
+
15
+
16
+ @singleton
17
+ class WorkDirManager:
18
+ """Class for working directory manager.
19
+
20
+ This class is a singleton and is used for managing temporary and working
21
+ directories used across the pipeline by datagrabbers, preprocessors,
22
+ markers and so on. It maintains a single super-directory and provides
23
+ directories on-demand and cleans after itself thus keeping the user
24
+ filesystem clean.
25
+
26
+ Parameters
27
+ ----------
28
+ workdir : str or pathlib.Path, optional
29
+ The path to the super-directory. If None, "TMPDIR/junifer" is used
30
+ where TMPDIR is the platform-dependent temporary directory.
31
+
32
+ Attributes
33
+ ----------
34
+ workdir : pathlib.Path
35
+ The path to the working directory.
36
+ elementdir : pathlib.Path
37
+ The path to the element directory.
38
+ root_tempdir : pathlib.Path or None
39
+ The path to the root temporary directory.
40
+
41
+ """
42
+
43
+ def __init__(self, workdir: Optional[Union[str, Path]] = None) -> None:
44
+ """Initialize the class."""
45
+ self._workdir = Path(workdir) if isinstance(workdir, str) else workdir
46
+ self._elementdir = None
47
+ self._root_tempdir = None
48
+
49
+ self._set_default_workdir()
50
+
51
+ def _set_default_workdir(self) -> None:
52
+ """Set the default working directory if not set already."""
53
+ # Check and set topmost level directory if not provided
54
+ if self._workdir is None:
55
+ self._workdir = Path(tempfile.gettempdir()) / "junifer"
56
+ # Create directory if not found
57
+ if not self._workdir.is_dir():
58
+ logger.debug(
59
+ "Creating working directory at "
60
+ f"{self._workdir.resolve()!s}"
61
+ )
62
+ self._workdir.mkdir(parents=True)
63
+ logger.debug(
64
+ f"Setting working directory to {self._workdir.resolve()!s}"
65
+ )
66
+
67
+ def __del__(self) -> None:
68
+ """Destructor."""
69
+ self._cleanup()
70
+
71
+ def _cleanup(self) -> None:
72
+ """Clean up the element and temporary directories."""
73
+ # Remove element directory
74
+ self.cleanup_elementdir()
75
+ # Remove root temporary directory
76
+ if self._root_tempdir is not None:
77
+ logger.debug(
78
+ "Deleting temporary directory at "
79
+ f"{self._root_tempdir.resolve()!s}"
80
+ )
81
+ shutil.rmtree(self._root_tempdir, ignore_errors=True)
82
+ self._root_tempdir = None
83
+
84
+ @property
85
+ def workdir(self) -> Path:
86
+ """Get working directory."""
87
+ return self._workdir # type: ignore
88
+
89
+ @workdir.setter
90
+ def workdir(self, path: Union[str, Path]) -> None:
91
+ """Set working directory.
92
+
93
+ The directory path is created if it doesn't exist yet.
94
+
95
+ Parameters
96
+ ----------
97
+ path : str or pathlib.Path
98
+ The path to the working directory.
99
+
100
+ """
101
+ # Check if existing working directory is same or not;
102
+ # if not, then clean up
103
+ if self._workdir != Path(path):
104
+ self._cleanup()
105
+ # Set working directory
106
+ self._workdir = Path(path)
107
+ logger.debug(
108
+ f"Changing working directory to {self._workdir.resolve()!s}"
109
+ )
110
+ # Create directory if not found
111
+ if not self._workdir.is_dir():
112
+ logger.debug(
113
+ f"Creating working directory at {self._workdir.resolve()!s}"
114
+ )
115
+ self._workdir.mkdir(parents=True)
116
+
117
+ @property
118
+ def elementdir(self) -> Path:
119
+ """Get element directory."""
120
+ return self._elementdir # type: ignore
121
+
122
+ def get_element_tempdir(
123
+ self, prefix: Optional[str] = None, suffix: Optional[str] = None
124
+ ) -> Path:
125
+ """Get an element-scoped temporary directory.
126
+
127
+ This directory should be available only for the lifetime of an
128
+ element.
129
+
130
+ Parameters
131
+ ----------
132
+ prefix : str, optional
133
+ The temporary directory prefix. If None, a default prefix is used
134
+ (default None).
135
+ suffix : str, optional
136
+ The temporary directory suffix. If None, no suffix is added
137
+ (default None).
138
+
139
+ Returns
140
+ -------
141
+ pathlib.Path
142
+ The path to the temporary directory.
143
+
144
+ """
145
+ # Create element directory if not created already
146
+ if self._elementdir is None:
147
+ logger.debug(
148
+ "Setting up element directory under "
149
+ f"{self._workdir.resolve()!s}" # type: ignore
150
+ )
151
+ self._elementdir = Path(tempfile.mkdtemp(dir=self._workdir))
152
+
153
+ logger.debug(
154
+ "Creating element temporary directory at "
155
+ f"{self._elementdir.resolve()!s}"
156
+ )
157
+ return Path(
158
+ tempfile.mkdtemp(
159
+ dir=self._elementdir, prefix=prefix, suffix=suffix
160
+ )
161
+ )
162
+
163
+ def delete_element_tempdir(self, tempdir: Path) -> None:
164
+ """Delete an element-scoped temporary directory.
165
+
166
+ Parameters
167
+ ----------
168
+ tempdir : pathlib.Path
169
+ The temporary directory path to be deleted.
170
+
171
+ """
172
+ logger.debug(f"Deleting element temporary directory at {tempdir}")
173
+ shutil.rmtree(tempdir, ignore_errors=True)
174
+
175
+ def cleanup_elementdir(self) -> None:
176
+ """Clean up element directory.
177
+
178
+ It should preferably be used after fitting a marker or something
179
+ similar in the element-specific scope. If called between components,
180
+ can lead to required intermediate files not being found.
181
+
182
+ """
183
+ if self._elementdir is not None:
184
+ logger.debug(
185
+ "Deleting element directory at "
186
+ f"{self._elementdir.resolve()!s}"
187
+ )
188
+ shutil.rmtree(self._elementdir, ignore_errors=True)
189
+ self._elementdir = None
190
+
191
+ @property
192
+ def root_tempdir(self) -> Optional[Path]:
193
+ """Get root temporary directory."""
194
+ return self._root_tempdir
195
+
196
+ def get_tempdir(
197
+ self, prefix: Optional[str] = None, suffix: Optional[str] = None
198
+ ) -> Path:
199
+ """Get a component-scoped temporary directory.
200
+
201
+ This directory should be available only for the lifetime of a component
202
+ like a preprocessor or marker.
203
+
204
+ Parameters
205
+ ----------
206
+ prefix : str, optional
207
+ The temporary directory prefix. If None, a default prefix is used
208
+ (default None).
209
+ suffix : str, optional
210
+ The temporary directory suffix. If None, no suffix is added
211
+ (default None).
212
+
213
+ Returns
214
+ -------
215
+ pathlib.Path
216
+ The path to the temporary directory.
217
+
218
+ """
219
+ # Create root temporary directory if not created already
220
+ if self._root_tempdir is None:
221
+ logger.debug(
222
+ "Setting up temporary directory under "
223
+ f"{self._workdir.resolve()!s}" # type: ignore
224
+ )
225
+ self._root_tempdir = Path(tempfile.mkdtemp(dir=self._workdir))
226
+
227
+ logger.debug(
228
+ f"Creating temporary directory at {self._root_tempdir.resolve()!s}"
229
+ )
230
+ return Path(
231
+ tempfile.mkdtemp(
232
+ dir=self._root_tempdir, prefix=prefix, suffix=suffix
233
+ )
234
+ )
235
+
236
+ def delete_tempdir(self, tempdir: Path) -> None:
237
+ """Delete a component-scoped temporary directory.
238
+
239
+ Parameters
240
+ ----------
241
+ tempdir : pathlib.Path
242
+ The temporary directory path to be deleted.
243
+
244
+ """
245
+ logger.debug(f"Deleting temporary directory at {tempdir}")
246
+ shutil.rmtree(tempdir, ignore_errors=True)
@@ -2,7 +2,10 @@
2
2
 
3
3
  # Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
4
4
  # Leonard Sasse <l.sasse@fz-juelich.de>
5
+ # Synchon Mandal <s.mandal@fz-juelich.de>
5
6
  # License: AGPL
6
7
 
7
8
  from .base import BasePreprocessor
8
9
  from .confounds import fMRIPrepConfoundRemover
10
+ from .bold_warper import BOLDWarper
11
+ from .warping import SpaceWarper
@@ -0,0 +1,4 @@
1
+ """Provide imports for ants sub-package."""
2
+
3
+ # Authors: Synchon Mandal <s.mandal@fz-juelich.de>
4
+ # License: AGPL
@@ -0,0 +1,185 @@
1
+ """Provide class for warping via ANTs antsApplyTransforms."""
2
+
3
+ # Authors: Synchon Mandal <s.mandal@fz-juelich.de>
4
+ # License: AGPL
5
+
6
+ from pathlib import Path
7
+ from typing import (
8
+ TYPE_CHECKING,
9
+ Any,
10
+ ClassVar,
11
+ Dict,
12
+ List,
13
+ Optional,
14
+ Tuple,
15
+ Union,
16
+ )
17
+
18
+ import nibabel as nib
19
+ import numpy as np
20
+
21
+ from ...pipeline import WorkDirManager
22
+ from ...utils import logger, raise_error, run_ext_cmd
23
+
24
+
25
+ if TYPE_CHECKING:
26
+ from nibabel import Nifti1Image
27
+
28
+
29
+ class _AntsApplyTransformsWarper:
30
+ """Class for warping NIfTI images via ANTs antsApplyTransforms.
31
+
32
+ Warps ANTs ``antsApplyTransforms``.
33
+
34
+ Parameters
35
+ ----------
36
+ reference : str
37
+ The data type to use as reference for warping.
38
+ on : str
39
+ The data type to use for warping.
40
+
41
+ Raises
42
+ ------
43
+ ValueError
44
+ If a list was passed for ``on``.
45
+
46
+ """
47
+
48
+ _EXT_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, List[str]]]]] = [
49
+ {
50
+ "name": "ants",
51
+ "commands": ["ResampleImage", "antsApplyTransforms"],
52
+ },
53
+ ]
54
+
55
+ def __init__(self, reference: str, on: str) -> None:
56
+ """Initialize the class."""
57
+ self.ref = reference
58
+ # Check only single data type is passed
59
+ if isinstance(on, list):
60
+ raise_error("Can only work on single data type, list was passed.")
61
+ self.on = on
62
+
63
+ def _run_apply_transforms(
64
+ self,
65
+ input_data: Dict,
66
+ ref_path: Path,
67
+ warp_path: Path,
68
+ ) -> Tuple["Nifti1Image", Path]:
69
+ """Run ``antsApplyTransforms``.
70
+
71
+ Parameters
72
+ ----------
73
+ input_data : dict
74
+ The input data.
75
+ ref_path : pathlib.Path
76
+ The path to the reference file.
77
+ warp_path : pathlib.Path
78
+ The path to the warp file.
79
+
80
+ Returns
81
+ -------
82
+ Niimg-like object
83
+ The warped input image.
84
+ pathlib.Path
85
+ The path to the resampled reference image.
86
+
87
+ """
88
+ # Get the min of the voxel sizes from input and use it as the
89
+ # resolution
90
+ resolution = np.min(input_data["data"].header.get_zooms()[:3])
91
+
92
+ # Create element-specific tempdir for storing post-warping assets
93
+ tempdir = WorkDirManager().get_element_tempdir(
94
+ prefix="applytransforms"
95
+ )
96
+
97
+ # Create a tempfile for resampled reference output
98
+ resample_image_out_path = tempdir / "reference_resampled.nii.gz"
99
+ # Set ResampleImage command
100
+ resample_image_cmd = [
101
+ "ResampleImage",
102
+ "3", # image dimension
103
+ f"{ref_path.resolve()}",
104
+ f"{resample_image_out_path.resolve()}",
105
+ f"{resolution}x{resolution}x{resolution}",
106
+ "0", # option for spacing and not size
107
+ "3 3", # Lanczos windowed sinc
108
+ ]
109
+ # Call ResampleImage
110
+ run_ext_cmd(name="ResampleImage", cmd=resample_image_cmd)
111
+
112
+ # Create a tempfile for warped output
113
+ apply_transforms_out_path = tempdir / "input_warped.nii.gz"
114
+ # Set antsApplyTransforms command
115
+ apply_transforms_cmd = [
116
+ "antsApplyTransforms",
117
+ "-d 3",
118
+ "-e 3",
119
+ "-n LanczosWindowedSinc",
120
+ f"-i {input_data['path'].resolve()}",
121
+ # use resampled reference
122
+ f"-r {resample_image_out_path.resolve()}",
123
+ f"-t {warp_path.resolve()}",
124
+ f"-o {apply_transforms_out_path.resolve()}",
125
+ ]
126
+ # Call antsApplyTransforms
127
+ run_ext_cmd(name="antsApplyTransforms", cmd=apply_transforms_cmd)
128
+
129
+ # Load nifti
130
+ output_img = nib.load(apply_transforms_out_path)
131
+
132
+ return output_img, resample_image_out_path # type: ignore
133
+
134
+ def preprocess(
135
+ self,
136
+ input: Dict[str, Any],
137
+ extra_input: Optional[Dict[str, Any]] = None,
138
+ ) -> Tuple[str, Dict[str, Any]]:
139
+ """Preprocess.
140
+
141
+ Parameters
142
+ ----------
143
+ input : dict
144
+ A single input from the Junifer Data object in which to preprocess.
145
+ extra_input : dict, optional
146
+ The other fields in the Junifer Data object. Must include the
147
+ ``Warp`` and ``ref`` value's keys.
148
+
149
+ Returns
150
+ -------
151
+ str
152
+ The key to store the output in the Junifer Data object.
153
+ dict
154
+ The computed result as dictionary. This will be stored in the
155
+ Junifer Data object under the key ``data`` of the data type.
156
+
157
+ Raises
158
+ ------
159
+ ValueError
160
+ If ``extra_input`` is None.
161
+
162
+ """
163
+ logger.debug("Warping via ANTs using antsApplyTransforms")
164
+ # Check for extra inputs
165
+ if extra_input is None:
166
+ raise_error(
167
+ f"No extra input provided, requires `Warp` and `{self.ref}` "
168
+ "data types in particular."
169
+ )
170
+ # Retrieve data type info to warp
171
+ to_warp_input = input
172
+ # Retrieve data type info to use as reference
173
+ ref_input = extra_input[self.ref]
174
+ # Retrieve Warp data
175
+ warp = extra_input["Warp"]
176
+ # Replace original data with warped data and add resampled reference
177
+ # path
178
+ input["data"], input["reference_path"] = self._run_apply_transforms(
179
+ input_data=to_warp_input,
180
+ ref_path=ref_input["path"],
181
+ warp_path=warp["path"],
182
+ )
183
+ # Use reference input's space as warped input's space
184
+ input["space"] = ref_input["space"]
185
+ return self.on, input