junifer 0.0.3.dev186__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.dev186.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.dev186.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.dev186.dist-info/RECORD +0 -199
  175. {junifer-0.0.3.dev186.dist-info → junifer-0.0.4.dist-info}/AUTHORS.rst +0 -0
  176. {junifer-0.0.3.dev186.dist-info → junifer-0.0.4.dist-info}/LICENSE.md +0 -0
  177. {junifer-0.0.3.dev186.dist-info → junifer-0.0.4.dist-info}/entry_points.txt +0 -0
  178. {junifer-0.0.3.dev186.dist-info → junifer-0.0.4.dist-info}/top_level.txt +0 -0
@@ -7,11 +7,11 @@
7
7
  from typing import Any, ClassVar, Dict, List, Optional, Set, Union
8
8
 
9
9
  import numpy as np
10
- from nilearn.image import math_img, resample_to_img
10
+ from nilearn.image import math_img
11
11
  from nilearn.maskers import NiftiMasker
12
12
 
13
13
  from ..api.decorators import register_marker
14
- from ..data import get_mask, load_parcellation, merge_parcellations
14
+ from ..data import get_mask, get_parcellation
15
15
  from ..stats import get_aggfunc_by_name
16
16
  from ..utils import logger, raise_error, warn_with_log
17
17
  from .base import BaseMarker
@@ -42,13 +42,20 @@ class ParcelAggregation(BaseMarker):
42
42
  The specification of the masks to apply to regions before extracting
43
43
  signals. Check :ref:`Using Masks <using_masks>` for more details.
44
44
  If None, will not apply any mask (default None).
45
- on : {"T1w", "BOLD", "VBM_GM", "VBM_WM", "fALFF", "GCOR", "LCOR"} \
46
- or list of the options, optional
45
+ on : {"T1w", "T2w", "BOLD", "VBM_GM", "VBM_WM", "VBM_CSF", "fALFF", \
46
+ "GCOR", "LCOR"} or list of the options, optional
47
47
  The data types to apply the marker to. If None, will work on all
48
48
  available data (default None).
49
49
  name : str, optional
50
50
  The name of the marker. If None, will use the class name (default
51
51
  None).
52
+
53
+ Raises
54
+ ------
55
+ ValueError
56
+ If ``time_method`` is specified for non-BOLD data or if
57
+ ``time_method_params`` is not None when ``time_method`` is None.
58
+
52
59
  """
53
60
 
54
61
  _DEPENDENCIES: ClassVar[Set[str]] = {"nilearn", "numpy"}
@@ -95,7 +102,17 @@ class ParcelAggregation(BaseMarker):
95
102
  The list of data types that can be used as input for this marker.
96
103
 
97
104
  """
98
- return ["T1w", "BOLD", "VBM_GM", "VBM_WM", "fALFF", "GCOR", "LCOR"]
105
+ return [
106
+ "T1w",
107
+ "T2w",
108
+ "BOLD",
109
+ "VBM_GM",
110
+ "VBM_WM",
111
+ "VBM_CSF",
112
+ "fALFF",
113
+ "GCOR",
114
+ "LCOR",
115
+ ]
99
116
 
100
117
  def get_output_type(self, input_type: str) -> str:
101
118
  """Get output type.
@@ -110,14 +127,26 @@ class ParcelAggregation(BaseMarker):
110
127
  str
111
128
  The storage type output by the marker.
112
129
 
130
+ Raises
131
+ ------
132
+ ValueError
133
+ If the ``input_type`` is invalid.
134
+
113
135
  """
114
136
 
115
- if input_type in ["VBM_GM", "VBM_WM", "fALFF", "GCOR", "LCOR"]:
137
+ if input_type in [
138
+ "VBM_GM",
139
+ "VBM_WM",
140
+ "VBM_CSF",
141
+ "fALFF",
142
+ "GCOR",
143
+ "LCOR",
144
+ ]:
116
145
  return "vector"
117
146
  elif input_type == "BOLD":
118
147
  return "timeseries"
119
148
  else:
120
- raise ValueError(f"Unknown input kind for {input_type}")
149
+ raise_error(f"Unknown input kind for {input_type}")
121
150
 
122
151
  def compute(
123
152
  self, input: Dict[str, Any], extra_input: Optional[Dict] = None
@@ -145,62 +174,53 @@ class ParcelAggregation(BaseMarker):
145
174
  * ``data`` : the actual computed values as a numpy.ndarray
146
175
  * ``col_names`` : the column labels for the computed values as list
147
176
 
177
+ Warns
178
+ -----
179
+ RuntimeWarning
180
+ If time aggregation is required but only time point is available.
181
+
148
182
  """
149
183
  t_input_img = input["data"]
150
184
  logger.debug(f"Parcel aggregation using {self.method}")
185
+ # Get aggregation function
151
186
  agg_func = get_aggfunc_by_name(
152
187
  name=self.method, func_params=self.method_params
153
188
  )
154
- # Get the min of the voxels sizes and use it as the resolution
155
- resolution = np.min(t_input_img.header.get_zooms()[:3])
156
-
157
- # Load the parcellations
158
- all_parcelations = []
159
- all_labels = []
160
- for t_parc_name in self.parcellation:
161
- t_parcellation, t_labels, _ = load_parcellation(
162
- name=t_parc_name, resolution=resolution
163
- )
164
- # Resample all of them to the image
165
- t_parcellation_img_res = resample_to_img(
166
- t_parcellation, t_input_img, interpolation="nearest", copy=True
167
- )
168
- all_parcelations.append(t_parcellation_img_res)
169
- all_labels.append(t_labels)
170
189
 
171
- # Avoid merging if there is only one parcellation
172
- if len(all_parcelations) == 1:
173
- parcellation_img_res = all_parcelations[0]
174
- labels = all_labels[0]
175
- else:
176
- # Merge the parcellations
177
- parcellation_img_res, labels = merge_parcellations(
178
- all_parcelations, self.parcellation, all_labels
179
- )
190
+ # Get parcellation tailored to target image
191
+ parcellation_img, labels = get_parcellation(
192
+ parcellation=self.parcellation,
193
+ target_data=input,
194
+ extra_input=extra_input,
195
+ )
180
196
 
181
- parcellation_bin = math_img("img != 0", img=parcellation_img_res)
197
+ # Get binarized parcellation image for masking
198
+ parcellation_bin = math_img("img != 0", img=parcellation_img)
182
199
 
200
+ # Load mask
183
201
  if self.masks is not None:
184
202
  logger.debug(f"Masking with {self.masks}")
203
+ # Get tailored mask
185
204
  mask_img = get_mask(
186
205
  masks=self.masks, target_data=input, extra_input=extra_input
187
206
  )
188
-
207
+ # Get "logical and" version of parcellation and mask
189
208
  parcellation_bin = math_img(
190
209
  "np.logical_and(img, mask)",
191
210
  img=parcellation_bin,
192
211
  mask=mask_img,
193
212
  )
194
213
 
214
+ # Initialize masker
195
215
  logger.debug("Masking")
196
216
  masker = NiftiMasker(
197
217
  parcellation_bin, target_affine=t_input_img.affine
198
- ) # type: ignore
199
-
218
+ )
200
219
  # Mask the input data and the parcellation
201
220
  data = masker.fit_transform(t_input_img)
202
- parcellation_values = masker.transform(parcellation_img_res)
203
- parcellation_values = np.squeeze(parcellation_values).astype(int)
221
+ parcellation_values = np.squeeze(
222
+ masker.transform(parcellation_img)
223
+ ).astype(int)
204
224
 
205
225
  # Get the values for each parcel and apply agg function
206
226
  logger.debug("Computing ROI means")
@@ -214,6 +234,7 @@ class ParcelAggregation(BaseMarker):
214
234
 
215
235
  out_values = np.array(out_values).T
216
236
 
237
+ # Apply time dimension aggregation if required
217
238
  if self.time_method is not None:
218
239
  if out_values.shape[0] > 1:
219
240
  logger.debug("Aggregating time dimension")
@@ -226,5 +247,6 @@ class ParcelAggregation(BaseMarker):
226
247
  "No time dimension to aggregate as only one time point is "
227
248
  "available."
228
249
  )
250
+ # Format the output
229
251
  out = {"data": out_values, "col_names": labels}
230
252
  return out
@@ -0,0 +1,192 @@
1
+ """Provide class for computing regional homogeneity (ReHo) using AFNI."""
2
+
3
+ # Authors: Synchon Mandal <s.mandal@fz-juelich.de>
4
+ # License: AGPL
5
+
6
+ from functools import lru_cache
7
+ from pathlib import Path
8
+ from typing import (
9
+ TYPE_CHECKING,
10
+ ClassVar,
11
+ Dict,
12
+ List,
13
+ Optional,
14
+ Tuple,
15
+ Union,
16
+ )
17
+
18
+ import nibabel as nib
19
+
20
+ from ...pipeline import WorkDirManager
21
+ from ...pipeline.singleton import singleton
22
+ from ...utils import logger, run_ext_cmd
23
+
24
+
25
+ if TYPE_CHECKING:
26
+ from nibabel import Nifti1Image
27
+
28
+
29
+ @singleton
30
+ class AFNIReHo:
31
+ """Class for computing ReHo using AFNI.
32
+
33
+ This class uses AFNI's 3dReHo to compute ReHo. It's designed as a singleton
34
+ with caching for efficient computation.
35
+
36
+ """
37
+
38
+ _EXT_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, List[str]]]]] = [
39
+ {
40
+ "name": "afni",
41
+ "commands": ["3dReHo", "3dAFNItoNIFTI"],
42
+ },
43
+ ]
44
+
45
+ def __del__(self) -> None:
46
+ """Terminate the class."""
47
+ # Clear the computation cache
48
+ logger.debug("Clearing cache for ReHo computation via AFNI")
49
+ self.compute.cache_clear()
50
+
51
+ @lru_cache(maxsize=None, typed=True)
52
+ def compute(
53
+ self,
54
+ data: "Nifti1Image",
55
+ nneigh: int = 27,
56
+ neigh_rad: Optional[float] = None,
57
+ neigh_x: Optional[float] = None,
58
+ neigh_y: Optional[float] = None,
59
+ neigh_z: Optional[float] = None,
60
+ box_rad: Optional[int] = None,
61
+ box_x: Optional[int] = None,
62
+ box_y: Optional[int] = None,
63
+ box_z: Optional[int] = None,
64
+ ) -> Tuple["Nifti1Image", Path]:
65
+ """Compute ReHo map.
66
+
67
+ Parameters
68
+ ----------
69
+ data : 4D Niimg-like object
70
+ Images to process.
71
+ nneigh : {7, 19, 27}, optional
72
+ Number of voxels in the neighbourhood, inclusive. Can be:
73
+
74
+ * 7 : for facewise neighbours only
75
+ * 19 : for face- and edge-wise nieghbours
76
+ * 27 : for face-, edge-, and node-wise neighbors
77
+
78
+ (default 27).
79
+ neigh_rad : positive float, optional
80
+ The radius of a desired neighbourhood (default None).
81
+ neigh_x : positive float, optional
82
+ The semi-radius for x-axis of ellipsoidal volumes (default None).
83
+ neigh_y : positive float, optional
84
+ The semi-radius for y-axis of ellipsoidal volumes (default None).
85
+ neigh_z : positive float, optional
86
+ The semi-radius for z-axis of ellipsoidal volumes (default None).
87
+ box_rad : positive int, optional
88
+ The number of voxels outward in a given cardinal direction for a
89
+ cubic box centered on a given voxel (default None).
90
+ box_x : positive int, optional
91
+ The number of voxels for +/- x-axis of cuboidal volumes
92
+ (default None).
93
+ box_y : positive int, optional
94
+ The number of voxels for +/- y-axis of cuboidal volumes
95
+ (default None).
96
+ box_z : positive int, optional
97
+ The number of voxels for +/- z-axis of cuboidal volumes
98
+ (default None).
99
+
100
+ Returns
101
+ -------
102
+ Niimg-like object
103
+ The ReHo map as NIfTI.
104
+ pathlib.Path
105
+ The path to the ReHo map as NIfTI.
106
+
107
+ Notes
108
+ -----
109
+ For more information on the publication, please check [1]_ , and for
110
+ 3dReHo help check:
111
+ https://afni.nimh.nih.gov/pub/dist/doc/program_help/3dReHo.html
112
+
113
+ Please note that that you cannot mix ``box_*`` and ``neigh_*``
114
+ arguments. The arguments are prioritized by their order in the function
115
+ signature.
116
+
117
+ As the process also depends on the conversion of AFNI files to NIFTI
118
+ via afni's 3dAFNItoNIFTI, the help for that can be found at:
119
+ https://afni.nimh.nih.gov/pub/dist/doc/program_help/3dAFNItoNIFTI.html
120
+
121
+ References
122
+ ----------
123
+ .. [1] Taylor, P.A., & Saad, Z.S. (2013).
124
+ FATCAT: (An Efficient) Functional And Tractographic Connectivity
125
+ Analysis Toolbox.
126
+ Brain connectivity, Volume 3(5), Pages 523-35.
127
+ https://doi.org/10.1089/brain.2013.0154
128
+
129
+ """
130
+ logger.debug("Creating cache for ReHo computation via AFNI")
131
+
132
+ # Create component-scoped tempdir
133
+ tempdir = WorkDirManager().get_tempdir(prefix="afni_reho")
134
+
135
+ # Save target data to a component-scoped tempfile
136
+ nifti_in_file_path = tempdir / "input.nii" # needs to be .nii
137
+ nib.save(data, nifti_in_file_path)
138
+
139
+ # Set 3dReHo command
140
+ reho_out_path_prefix = tempdir / "reho"
141
+ reho_cmd = [
142
+ "3dReHo",
143
+ f"-prefix {reho_out_path_prefix.resolve()}",
144
+ f"-inset {nifti_in_file_path.resolve()}",
145
+ ]
146
+ # Check ellipsoidal / cuboidal volume arguments
147
+ if neigh_rad:
148
+ reho_cmd.append(f"-neigh_RAD {neigh_rad}")
149
+ elif neigh_x and neigh_y and neigh_z:
150
+ reho_cmd.extend(
151
+ [
152
+ f"-neigh_X {neigh_x}",
153
+ f"-neigh_Y {neigh_y}",
154
+ f"-neigh_Z {neigh_z}",
155
+ ]
156
+ )
157
+ elif box_rad:
158
+ reho_cmd.append(f"-box_RAD {box_rad}")
159
+ elif box_x and box_y and box_z:
160
+ reho_cmd.extend(
161
+ [f"-box_X {box_x}", f"-box_Y {box_y}", f"-box_Z {box_z}"]
162
+ )
163
+ else:
164
+ reho_cmd.append(f"-nneigh {nneigh}")
165
+ # Call 3dReHo
166
+ run_ext_cmd(name="3dReHo", cmd=reho_cmd)
167
+
168
+ # Create element-scoped tempdir so that the ReHo map is
169
+ # available later as nibabel stores file path reference for
170
+ # loading on computation
171
+ element_tempdir = WorkDirManager().get_element_tempdir(
172
+ prefix="afni_reho"
173
+ )
174
+ # Convert afni to nifti
175
+ reho_afni_to_nifti_out_path = (
176
+ element_tempdir / "output.nii" # needs to be .nii
177
+ )
178
+ convert_cmd = [
179
+ "3dAFNItoNIFTI",
180
+ f"-prefix {reho_afni_to_nifti_out_path.resolve()}",
181
+ f"{reho_out_path_prefix}+tlrc.BRIK",
182
+ ]
183
+ # Call 3dAFNItoNIFTI
184
+ run_ext_cmd(name="3dAFNItoNIFTI", cmd=convert_cmd)
185
+
186
+ # Load nifti
187
+ output_data = nib.load(reho_afni_to_nifti_out_path)
188
+
189
+ # Delete tempdir
190
+ WorkDirManager().delete_tempdir(tempdir)
191
+
192
+ return output_data, reho_afni_to_nifti_out_path # type: ignore
@@ -0,0 +1,281 @@
1
+ """Provide class for computing regional homogeneity (ReHo) using junifer."""
2
+
3
+ # Authors: Synchon Mandal <s.mandal@fz-juelich.de>
4
+ # License: AGPL
5
+
6
+ from functools import lru_cache
7
+ from itertools import product
8
+ from pathlib import Path
9
+ from typing import (
10
+ TYPE_CHECKING,
11
+ ClassVar,
12
+ Set,
13
+ Tuple,
14
+ )
15
+
16
+ import nibabel as nib
17
+ import numpy as np
18
+ import scipy as sp
19
+ from nilearn import image as nimg
20
+ from nilearn import masking as nmask
21
+
22
+ from ...pipeline import WorkDirManager
23
+ from ...pipeline.singleton import singleton
24
+ from ...utils import logger, raise_error
25
+
26
+
27
+ if TYPE_CHECKING:
28
+ from nibabel import Nifti1Image
29
+
30
+
31
+ @singleton
32
+ class JuniferReHo:
33
+ """Class for computing ReHo using junifer.
34
+
35
+ It's designed as a singleton with caching for efficient computation.
36
+
37
+ """
38
+
39
+ _DEPENDENCIES: ClassVar[Set[str]] = {"numpy", "nilearn", "scipy"}
40
+
41
+ def __del__(self) -> None:
42
+ """Terminate the class."""
43
+ # Clear the computation cache
44
+ logger.debug("Clearing cache for ReHo computation via junifer")
45
+ self.compute.cache_clear()
46
+
47
+ @lru_cache(maxsize=None, typed=True)
48
+ def compute(
49
+ self,
50
+ data: "Nifti1Image",
51
+ nneigh: int = 27,
52
+ ) -> Tuple["Nifti1Image", Path]:
53
+ """Compute ReHo map.
54
+
55
+ Parameters
56
+ ----------
57
+ data : 4D Niimg-like object
58
+ Images to process.
59
+ nneigh : {7, 19, 27, 125}, optional
60
+ Number of voxels in the neighbourhood, inclusive. Can be:
61
+
62
+ * 7 : for facewise neighbours only
63
+ * 19 : for face- and edge-wise nieghbours
64
+ * 27 : for face-, edge-, and node-wise neighbors
65
+ * 125 : for 5x5 cuboidal volume
66
+
67
+ (default 27).
68
+
69
+ Returns
70
+ -------
71
+ Niimg-like object
72
+ The ReHo map as NIfTI.
73
+ pathlib.Path
74
+ The path to the ReHo map as NIfTI.
75
+
76
+ Raises
77
+ ------
78
+ ValueError
79
+ If ``nneigh`` is invalid.
80
+
81
+ """
82
+ valid_nneigh = (7, 19, 27, 125)
83
+ if nneigh not in valid_nneigh:
84
+ raise_error(
85
+ f"Invalid value for `nneigh`, should be one of: {valid_nneigh}"
86
+ )
87
+
88
+ logger.debug("Creating cache for ReHo computation via junifer")
89
+
90
+ # Get scan data
91
+ niimg_data = data.get_fdata()
92
+ # Get scan dimensions
93
+ n_x, n_y, n_z, _ = niimg_data.shape
94
+
95
+ # Get rank of every voxel across time series
96
+ ranks_niimg_data = sp.stats.rankdata(niimg_data, axis=-1)
97
+
98
+ # Initialize 3D array to store tied rank correction for every voxel
99
+ tied_rank_corrections = np.zeros((n_x, n_y, n_z), dtype=np.float64)
100
+ # Calculate tied rank correction for every voxel
101
+ for i_x, i_y, i_z in product(range(n_x), range(n_y), range(n_z)):
102
+ # Calculate tied rank count for every voxel across time series
103
+ _, tie_count = np.unique(
104
+ ranks_niimg_data[i_x, i_y, i_z, :],
105
+ return_counts=True,
106
+ )
107
+ # Calculate and store tied rank correction for every voxel across
108
+ # timeseries
109
+ tied_rank_corrections[i_x, i_y, i_z] = np.sum(
110
+ tie_count**3 - tie_count
111
+ )
112
+
113
+ # Initialize 3D array to store reho map
114
+ reho_map = np.ones((n_x, n_y, n_z), dtype=np.float32)
115
+
116
+ # TODO(synchon): this will give incorrect results if
117
+ # template doesn't match, hence needs to be changed
118
+ # after #299 is merged
119
+ # Calculate whole brain mask
120
+ mni152_whole_brain_mask = nmask.compute_brain_mask(
121
+ target_img=data,
122
+ threshold=0.5,
123
+ mask_type="whole-brain",
124
+ )
125
+ # Convert 0 / 1 array to bool
126
+ logical_mni152_whole_brain_mask = (
127
+ mni152_whole_brain_mask.get_fdata().astype(bool)
128
+ )
129
+
130
+ # Create mask cluster and set start and end indices
131
+ if nneigh in (7, 19, 27):
132
+ mask_cluster = np.ones((3, 3, 3))
133
+
134
+ if nneigh == 7:
135
+ mask_cluster[0, 0, 0] = 0
136
+ mask_cluster[0, 1, 0] = 0
137
+ mask_cluster[0, 2, 0] = 0
138
+ mask_cluster[0, 0, 1] = 0
139
+ mask_cluster[0, 2, 1] = 0
140
+ mask_cluster[0, 0, 2] = 0
141
+ mask_cluster[0, 1, 2] = 0
142
+ mask_cluster[0, 2, 2] = 0
143
+ mask_cluster[1, 0, 0] = 0
144
+ mask_cluster[1, 2, 0] = 0
145
+ mask_cluster[1, 0, 2] = 0
146
+ mask_cluster[1, 2, 2] = 0
147
+ mask_cluster[2, 0, 0] = 0
148
+ mask_cluster[2, 1, 0] = 0
149
+ mask_cluster[2, 2, 0] = 0
150
+ mask_cluster[2, 0, 1] = 0
151
+ mask_cluster[2, 2, 1] = 0
152
+ mask_cluster[2, 0, 2] = 0
153
+ mask_cluster[2, 1, 2] = 0
154
+ mask_cluster[2, 2, 2] = 0
155
+
156
+ elif nneigh == 19:
157
+ mask_cluster[0, 0, 0] = 0
158
+ mask_cluster[0, 2, 0] = 0
159
+ mask_cluster[2, 0, 0] = 0
160
+ mask_cluster[2, 2, 0] = 0
161
+ mask_cluster[0, 0, 2] = 0
162
+ mask_cluster[0, 2, 2] = 0
163
+ mask_cluster[2, 0, 2] = 0
164
+ mask_cluster[2, 2, 2] = 0
165
+
166
+ start_idx = 1
167
+ end_idx = 2
168
+
169
+ elif nneigh == 125:
170
+ mask_cluster = np.ones((5, 5, 5))
171
+ start_idx = 2
172
+ end_idx = 3
173
+
174
+ # Convert 0 / 1 array to bool
175
+ logical_mask_cluster = mask_cluster.astype(bool)
176
+
177
+ for i, j, k in product(
178
+ range(start_idx, n_x - (end_idx - 1)),
179
+ range(start_idx, n_y - (end_idx - 1)),
180
+ range(start_idx, n_z - (end_idx - 1)),
181
+ ):
182
+ # Get mask only for neighbourhood
183
+ logical_neighbourhood_mni152_whole_brain_mask = (
184
+ logical_mni152_whole_brain_mask[
185
+ i - start_idx : i + end_idx,
186
+ j - start_idx : j + end_idx,
187
+ k - start_idx : k + end_idx,
188
+ ]
189
+ )
190
+ # Perform logical AND to get neighbourhood mask;
191
+ # done to take care of brain boundaries
192
+ neighbourhood_mask = (
193
+ logical_mask_cluster
194
+ & logical_neighbourhood_mni152_whole_brain_mask
195
+ )
196
+ # Continue if voxel is restricted by mask
197
+ if neighbourhood_mask[1, 1, 1] == 0:
198
+ continue
199
+
200
+ # Get ranks for the neighbourhood
201
+ neighbourhood_ranks = ranks_niimg_data[
202
+ i - start_idx : i + end_idx,
203
+ j - start_idx : j + end_idx,
204
+ k - start_idx : k + end_idx,
205
+ :,
206
+ ]
207
+ # Get tied ranks corrections for the neighbourhood
208
+ neighbourhood_tied_ranks_corrections = tied_rank_corrections[
209
+ i - start_idx : i + end_idx,
210
+ j - start_idx : j + end_idx,
211
+ k - start_idx : k + end_idx,
212
+ ]
213
+ # Mask neighbourhood ranks
214
+ masked_neighbourhood_ranks = neighbourhood_ranks[
215
+ logical_mask_cluster, :
216
+ ]
217
+ # Mask tied ranks corrections for the neighbourhood
218
+ masked_tied_rank_corrections = (
219
+ neighbourhood_tied_ranks_corrections[logical_mask_cluster]
220
+ )
221
+ # Calculate KCC
222
+ reho_map[i, j, k] = _kendall_w_reho(
223
+ timeseries_ranks=masked_neighbourhood_ranks,
224
+ tied_rank_corrections=masked_tied_rank_corrections,
225
+ )
226
+
227
+ # Create new image like target image
228
+ output_data = nimg.new_img_like(
229
+ ref_niimg=data,
230
+ data=reho_map,
231
+ copy_header=False,
232
+ )
233
+
234
+ # Create element-scoped tempdir so that the ReHo map is
235
+ # available later as nibabel stores file path reference for
236
+ # loading on computation
237
+ element_tempdir = WorkDirManager().get_element_tempdir(
238
+ prefix="junifer_reho"
239
+ )
240
+ output_path = element_tempdir / "output.nii.gz"
241
+ # Save computed data to file
242
+ nib.save(output_data, output_path)
243
+
244
+ return output_data, output_path # type: ignore
245
+
246
+
247
+ def _kendall_w_reho(
248
+ timeseries_ranks: np.ndarray, tied_rank_corrections: np.ndarray
249
+ ) -> float:
250
+ """Calculate Kendall's coefficient of concordance (KCC) for ReHo map.
251
+
252
+ ..note:: This function should only be used to calculate KCC for a ReHo map.
253
+ For general use, check out ``junifer.stats.kendall_w``.
254
+
255
+ Parameters
256
+ ----------
257
+ timeseries_ranks : 2D numpy.ndarray
258
+ A matrix of ranks of a subset subject's brain voxels.
259
+ tied_rank_corrections : 3D numpy.ndarray
260
+ A 3D array consisting of the tied rank corrections for the ranks
261
+ of a subset subject's brain voxels.
262
+
263
+ Returns
264
+ -------
265
+ float
266
+ Kendall's W (KCC) of the given timeseries matrix.
267
+
268
+ """
269
+ m, n = timeseries_ranks.shape # annotators X items
270
+
271
+ numerator = (12 * np.sum(np.square(np.sum(timeseries_ranks, axis=0)))) - (
272
+ 3 * m**2 * n * (n + 1) ** 2
273
+ )
274
+ denominator = (m**2 * n * (n**2 - 1)) - (m * np.sum(tied_rank_corrections))
275
+
276
+ if denominator == 0:
277
+ kcc = 1.0
278
+ else:
279
+ kcc = numerator / denominator
280
+
281
+ return kcc