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
@@ -0,0 +1,68 @@
1
+ """Provide test for sample entropy."""
2
+
3
+ # Authors: Amir Omidvarnia <a.omidvarnia@fz-juelich.de>
4
+ # Synchon Mandal <s.mandal@fz-juelich.de>
5
+ # License: AGPL
6
+
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+
12
+ pytest.importorskip("neurokit2")
13
+
14
+ from junifer.datareader import DefaultDataReader # noqa: E402
15
+ from junifer.markers.complexity import SampleEntropy # noqa: E402
16
+ from junifer.storage import SQLiteFeatureStorage # noqa: E402
17
+ from junifer.testing.datagrabbers import ( # noqa: E402
18
+ SPMAuditoryTestingDataGrabber,
19
+ )
20
+
21
+
22
+ # Set parcellation
23
+ PARCELLATION = "Schaefer100x17"
24
+
25
+
26
+ def test_compute() -> None:
27
+ """Test SampleEntropy compute()."""
28
+ with SPMAuditoryTestingDataGrabber() as dg:
29
+ # Fetch element
30
+ element = dg["sub001"]
31
+ # Fetch element data
32
+ element_data = DefaultDataReader().fit_transform(element)
33
+ # Initialize the marker
34
+ marker = SampleEntropy(parcellation=PARCELLATION)
35
+ # Compute the marker
36
+ feature_map = marker.fit_transform(element_data)
37
+ # Assert the dimension of timeseries
38
+ assert feature_map["BOLD"]["data"].ndim == 2
39
+
40
+
41
+ def test_get_output_type() -> None:
42
+ """Test SampleEntropy get_output_type()."""
43
+ marker = SampleEntropy(parcellation=PARCELLATION)
44
+ assert marker.get_output_type("BOLD") == "vector"
45
+
46
+
47
+ def test_store(tmp_path: Path) -> None:
48
+ """Test SampleEntropy store().
49
+
50
+ Parameters
51
+ ----------
52
+ tmp_path : pathlib.Path
53
+ The path to the test directory.
54
+
55
+ """
56
+ with SPMAuditoryTestingDataGrabber() as dg:
57
+ # Fetch element
58
+ element = dg["sub001"]
59
+ # Fetch element data
60
+ element_data = DefaultDataReader().fit_transform(element)
61
+ # Initialize the marker
62
+ marker = SampleEntropy(parcellation=PARCELLATION)
63
+ # Create storage
64
+ storage = SQLiteFeatureStorage(
65
+ uri=tmp_path / "test_sample_entropy.sqlite"
66
+ )
67
+ # Compute the marker and store
68
+ marker.fit_transform(input=element_data, storage=storage)
@@ -0,0 +1,68 @@
1
+ """Provide test for weighted permutation entropy."""
2
+
3
+ # Authors: Amir Omidvarnia <a.omidvarnia@fz-juelich.de>
4
+ # Synchon Mandal <s.mandal@fz-juelich.de>
5
+ # License: AGPL
6
+
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+
12
+ pytest.importorskip("neurokit2")
13
+
14
+ from junifer.datareader import DefaultDataReader # noqa: E402
15
+ from junifer.markers.complexity import WeightedPermEntropy # noqa: E402
16
+ from junifer.storage import SQLiteFeatureStorage # noqa: E402
17
+ from junifer.testing.datagrabbers import ( # noqa: E402
18
+ SPMAuditoryTestingDataGrabber,
19
+ )
20
+
21
+
22
+ # Set parcellation
23
+ PARCELLATION = "Schaefer100x17"
24
+
25
+
26
+ def test_compute() -> None:
27
+ """Test WeightedPermEntropy compute()."""
28
+ with SPMAuditoryTestingDataGrabber() as dg:
29
+ # Fetch element
30
+ element = dg["sub001"]
31
+ # Fetch element data
32
+ element_data = DefaultDataReader().fit_transform(element)
33
+ # Initialize the marker
34
+ marker = WeightedPermEntropy(parcellation=PARCELLATION)
35
+ # Compute the marker
36
+ feature_map = marker.fit_transform(element_data)
37
+ # Assert the dimension of timeseries
38
+ assert feature_map["BOLD"]["data"].ndim == 2
39
+
40
+
41
+ def test_get_output_type() -> None:
42
+ """Test WeightedPermEntropy get_output_type()."""
43
+ marker = WeightedPermEntropy(parcellation=PARCELLATION)
44
+ assert marker.get_output_type("BOLD") == "vector"
45
+
46
+
47
+ def test_store(tmp_path: Path) -> None:
48
+ """Test WeightedPermEntropy store().
49
+
50
+ Parameters
51
+ ----------
52
+ tmp_path : pathlib.Path
53
+ The path to the test directory.
54
+
55
+ """
56
+ with SPMAuditoryTestingDataGrabber() as dg:
57
+ # Fetch element
58
+ element = dg["sub001"]
59
+ # Fetch element data
60
+ element_data = DefaultDataReader().fit_transform(element)
61
+ # Initialize the marker
62
+ marker = WeightedPermEntropy(parcellation=PARCELLATION)
63
+ # Create storage
64
+ storage = SQLiteFeatureStorage(
65
+ uri=tmp_path / "test_weighted_perm_entropy.sqlite"
66
+ )
67
+ # Compute the marker and store
68
+ marker.fit_transform(input=element_data, storage=storage)
@@ -0,0 +1,133 @@
1
+ """Provide class for weighted permutation entropy of a time series."""
2
+
3
+ # Authors: Amir Omidvarnia <a.omidvarnia@fz-juelich.de>
4
+ # Leonard Sasse <l.sasse@fz-juelich.de>
5
+ # License: AGPL
6
+
7
+ from typing import Dict, List, Optional, Union
8
+
9
+ import neurokit2 as nk
10
+ import numpy as np
11
+
12
+ from ...api.decorators import register_marker
13
+ from ...utils import logger, warn_with_log
14
+ from .complexity_base import ComplexityBase
15
+
16
+
17
+ @register_marker
18
+ class WeightedPermEntropy(ComplexityBase):
19
+ """Class for weighted permutation entropy of a time series.
20
+
21
+ Parameters
22
+ ----------
23
+ parcellation : str or list of str
24
+ The name(s) of the parcellation(s). Check valid options by calling
25
+ :func:`junifer.data.parcellations.list_parcellations`.
26
+ agg_method : str, optional
27
+ The method to perform aggregation using. Check valid options in
28
+ :func:`junifer.stats.get_aggfunc_by_name` (default "mean").
29
+ agg_method_params : dict, optional
30
+ Parameters to pass to the aggregation function. Check valid options in
31
+ :func:`junifer.stats.get_aggfunc_by_name` (default None).
32
+ masks : str, dict or list of dict or str, optional
33
+ The specification of the masks to apply to regions before extracting
34
+ signals. Check :ref:`Using Masks <using_masks>` for more details.
35
+ If None, will not apply any mask (default None).
36
+ params : dict, optional
37
+ Parameters to pass to the weighted permutation entropy calculation
38
+ function.
39
+ For more information, check out
40
+ ``junifer.markers.utils._weighted_perm_entropy``. If None, value
41
+ is set to {"m": 2, "delay": 1} (default None).
42
+ name : str, optional
43
+ The name of the marker. If None, it will use the class name
44
+ (default None).
45
+
46
+ Warnings
47
+ --------
48
+ This class is not automatically imported by junifer and requires you to
49
+ import it explicitly. You can do it programmatically by
50
+ ``from junifer.markers.complexity import WeightedPermEntropy`` or in the
51
+ YAML by ``with: junifer.markers.complexity``.
52
+
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ parcellation: Union[str, List[str]],
58
+ agg_method: str = "mean",
59
+ agg_method_params: Optional[Dict] = None,
60
+ masks: Union[str, Dict, List[Union[Dict, str]], None] = None,
61
+ params: Optional[Dict] = None,
62
+ name: Optional[str] = None,
63
+ ) -> None:
64
+ super().__init__(
65
+ parcellation=parcellation,
66
+ agg_method=agg_method,
67
+ agg_method_params=agg_method_params,
68
+ masks=masks,
69
+ name=name,
70
+ )
71
+ if params is None:
72
+ self.params = {"m": 4, "delay": 1}
73
+ else:
74
+ self.params = params
75
+
76
+ def compute_complexity(
77
+ self,
78
+ extracted_bold_values: np.ndarray,
79
+ ) -> np.ndarray:
80
+ """Compute complexity measure.
81
+
82
+ Take a timeseries of brain areas, and calculate weighted permutation
83
+ entropy according to the method outlined in [1].
84
+
85
+ Parameters
86
+ ----------
87
+ extracted_bold_values : numpy.ndarray
88
+ The BOLD values extracted via parcel aggregation.
89
+
90
+ Returns
91
+ -------
92
+ numpy.ndarray
93
+ The values after computing complexity measure.
94
+
95
+ References
96
+ ----------
97
+ .. [1] Fadlallah, B., Chen, B., Keil, A., & Principe, J. (2013)
98
+ Weighted-permutation entropy: A complexity measure for
99
+ time series incorporating amplitude information.
100
+ Physical Review E, 87(2), 022911.
101
+
102
+ See Also
103
+ --------
104
+ neurokit2.entropy_permutation
105
+
106
+ """
107
+ logger.info("Calculating weighted permutation entropy.")
108
+
109
+ emb_dim = self.params["m"]
110
+ delay = self.params["delay"]
111
+
112
+ assert isinstance(emb_dim, int), "Embedding dimension must be integer."
113
+ assert isinstance(delay, int), "Delay must be integer."
114
+
115
+ _, n_roi = extracted_bold_values.shape
116
+ wperm_en_roi = np.zeros((n_roi, 1))
117
+
118
+ for idx_roi in range(n_roi):
119
+ sig = extracted_bold_values[:, idx_roi]
120
+ tmp = nk.entropy_permutation(
121
+ sig,
122
+ dimension=emb_dim,
123
+ delay=delay,
124
+ weighted=True, # Weighted PE
125
+ corrected=True, # Normalized PE
126
+ )
127
+
128
+ wperm_en_roi[idx_roi] = tmp[0]
129
+
130
+ if np.isnan(np.sum(wperm_en_roi)):
131
+ warn_with_log("There is NaN in the entropy values!")
132
+
133
+ return wperm_en_roi.T # 1 X n_roi
@@ -0,0 +1,153 @@
1
+ """Provide class for computing ALFF 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 AFNIALFF:
31
+ """Class for computing ALFF using AFNI.
32
+
33
+ This class uses AFNI's 3dRSFC to compute ALFF. 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": ["3dRSFC", "3dAFNItoNIFTI"],
42
+ },
43
+ ]
44
+
45
+ def __del__(self) -> None:
46
+ """Terminate the class."""
47
+ # Clear the computation cache
48
+ logger.debug("Clearing cache for ALFF 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
+ highpass: float,
56
+ lowpass: float,
57
+ tr: Optional[float],
58
+ ) -> Tuple["Nifti1Image", "Nifti1Image", Path, Path]:
59
+ """Compute ALFF + fALFF map.
60
+
61
+ Parameters
62
+ ----------
63
+ data : 4D Niimg-like object
64
+ Images to process.
65
+ highpass : positive float
66
+ Highpass cutoff frequency.
67
+ lowpass : positive float
68
+ Lowpass cutoff frequency.
69
+ tr : positive float, optional
70
+ The Repetition Time of the BOLD data.
71
+
72
+ Returns
73
+ -------
74
+ Niimg-like object
75
+ ALFF map.
76
+ Niimg-like object
77
+ fALFF map.
78
+ pathlib.Path
79
+ The path to the ALFF map as NIfTI.
80
+ pathlib.Path
81
+ The path to the fALFF map as NIfTI.
82
+
83
+ """
84
+ logger.debug("Creating cache for ALFF computation via AFNI")
85
+
86
+ # Create component-scoped tempdir
87
+ tempdir = WorkDirManager().get_tempdir(prefix="afni_alff+falff")
88
+
89
+ # Save target data to a component-scoped tempfile
90
+ nifti_in_file_path = tempdir / "input.nii" # needs to be .nii
91
+ nib.save(data, nifti_in_file_path)
92
+
93
+ # Set 3dRSFC command
94
+ alff_falff_out_path_prefix = tempdir / "alff_falff"
95
+ bp_cmd = [
96
+ "3dRSFC",
97
+ f"-prefix {alff_falff_out_path_prefix.resolve()}",
98
+ f"-input {nifti_in_file_path.resolve()}",
99
+ f"-band {highpass} {lowpass}",
100
+ "-no_rsfa -nosat -nodetrend",
101
+ ]
102
+ # Check tr
103
+ if tr is not None:
104
+ bp_cmd.append(f"-dt {tr}")
105
+ # Call 3dRSFC
106
+ run_ext_cmd(name="3dRSFC", cmd=bp_cmd)
107
+
108
+ # Create element-scoped tempdir so that the ALFF and fALFF maps are
109
+ # available later as nibabel stores file path reference for
110
+ # loading on computation
111
+ element_tempdir = WorkDirManager().get_element_tempdir(
112
+ prefix="afni_alff_falff"
113
+ )
114
+
115
+ params_suffix = f"_{highpass}_{lowpass}_{tr}"
116
+
117
+ # Convert alff afni to nifti
118
+ alff_afni_to_nifti_out_path = (
119
+ element_tempdir / f"alff{params_suffix}_output.nii"
120
+ ) # needs to be .nii
121
+ convert_alff_cmd = [
122
+ "3dAFNItoNIFTI",
123
+ f"-prefix {alff_afni_to_nifti_out_path.resolve()}",
124
+ f"{alff_falff_out_path_prefix}_ALFF+tlrc.BRIK",
125
+ ]
126
+ # Call 3dAFNItoNIFTI
127
+ run_ext_cmd(name="3dAFNItoNIFTI", cmd=convert_alff_cmd)
128
+
129
+ # Convert falff afni to nifti
130
+ falff_afni_to_nifti_out_path = (
131
+ element_tempdir / f"falff{params_suffix}_output.nii"
132
+ ) # needs to be .nii
133
+ convert_falff_cmd = [
134
+ "3dAFNItoNIFTI",
135
+ f"-prefix {falff_afni_to_nifti_out_path.resolve()}",
136
+ f"{alff_falff_out_path_prefix}_fALFF+tlrc.BRIK",
137
+ ]
138
+ # Call 3dAFNItoNIFTI
139
+ run_ext_cmd(name="3dAFNItoNIFTI", cmd=convert_falff_cmd)
140
+
141
+ # Load nifti
142
+ alff_data = nib.load(alff_afni_to_nifti_out_path)
143
+ falff_data = nib.load(falff_afni_to_nifti_out_path)
144
+
145
+ # Delete tempdir
146
+ WorkDirManager().delete_tempdir(tempdir)
147
+
148
+ return (
149
+ alff_data,
150
+ falff_data,
151
+ alff_afni_to_nifti_out_path,
152
+ falff_afni_to_nifti_out_path,
153
+ ) # type: ignore
@@ -0,0 +1,142 @@
1
+ """Provide class for computing ALFF using junifer."""
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
+ Optional,
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
+
21
+ from ...pipeline import WorkDirManager
22
+ from ...pipeline.singleton import singleton
23
+ from ...utils import logger
24
+
25
+
26
+ if TYPE_CHECKING:
27
+ from nibabel import Nifti1Image
28
+
29
+
30
+ @singleton
31
+ class JuniferALFF:
32
+ """Class for computing ALFF using junifer.
33
+
34
+ It's designed as a singleton with caching for efficient computation.
35
+
36
+ """
37
+
38
+ _DEPENDENCIES: ClassVar[Set[str]] = {"numpy", "nilearn", "scipy"}
39
+
40
+ def __del__(self) -> None:
41
+ """Terminate the class."""
42
+ # Clear the computation cache
43
+ logger.debug("Clearing cache for ALFF computation via junifer")
44
+ self.compute.cache_clear()
45
+
46
+ @lru_cache(maxsize=None, typed=True)
47
+ def compute(
48
+ self,
49
+ data: "Nifti1Image",
50
+ highpass: float,
51
+ lowpass: float,
52
+ tr: Optional[float],
53
+ ) -> Tuple["Nifti1Image", "Nifti1Image", Path, Path]:
54
+ """Compute ALFF + fALFF map.
55
+
56
+ Parameters
57
+ ----------
58
+ data : 4D Niimg-like object
59
+ Images to process.
60
+ highpass : positive float
61
+ Highpass cutoff frequency.
62
+ lowpass : positive float
63
+ Lowpass cutoff frequency.
64
+ tr : positive float, optional
65
+ The Repetition Time of the BOLD data.
66
+
67
+ Returns
68
+ -------
69
+ Niimg-like object
70
+ ALFF map.
71
+ Niimg-like object
72
+ fALFF map.
73
+ pathlib.Path
74
+ The path to the ALFF map as NIfTI.
75
+ pathlib.Path
76
+ The path to the fALFF map as NIfTI.
77
+
78
+ """
79
+ logger.debug("Creating cache for ALFF computation via junifer")
80
+
81
+ # Get scan data
82
+ niimg_data = data.get_fdata().copy()
83
+ if tr is None:
84
+ tr = float(data.header["pixdim"][4]) # type: ignore
85
+ logger.info(f"`tr` not provided, using `tr` from header: {tr}")
86
+
87
+ # Bandpass the data within the lowpass and highpass cutoff freqs
88
+ fft_data = sp.fft.fft(niimg_data, axis=-1)
89
+ fft_freqs = np.abs(sp.fft.fftfreq(niimg_data.shape[-1], tr))
90
+ # Frequency difference
91
+ fft_freqs_diff = fft_freqs[1] - fft_freqs[0]
92
+ # Nyquist frequency
93
+ nyquist = np.max(fft_freqs)
94
+ # FFT sample frequency count
95
+ n_fft = len(fft_freqs)
96
+ logger.info(
97
+ f"FFT: nfft = {n_fft}, dFreq = {fft_freqs_diff}, "
98
+ f"nyquist = {nyquist}"
99
+ )
100
+
101
+ # Compute the denominator on the broadband signal
102
+ all_freq_mask = fft_freqs > 0
103
+ denominator = np.sum(np.abs(fft_data[..., all_freq_mask]), axis=-1)
104
+
105
+ # Compute the numerator on the bandpassed signal
106
+ freq_mask = np.logical_and(fft_freqs > highpass, fft_freqs < lowpass)
107
+
108
+ # Compute ALFF
109
+ numerator = np.sum(np.abs(fft_data[..., freq_mask]), axis=-1)
110
+
111
+ # Compute fALFF, but avoid division by zero
112
+ denom_mask = denominator <= 0.000001
113
+ denominator[denom_mask] = 1 # set to 1 to avoid division by zero
114
+ # Calculate fALFF
115
+ falff = np.divide(numerator, denominator)
116
+ # Set the values where denominator is zero to zero
117
+ falff[denom_mask] = 0
118
+
119
+ # Calculate ALFF
120
+ alff = numerator / np.sqrt(niimg_data.shape[-1])
121
+ alff_data = nimg.new_img_like(
122
+ ref_niimg=data,
123
+ data=alff,
124
+ )
125
+ falff_data = nimg.new_img_like(
126
+ ref_niimg=data,
127
+ data=falff,
128
+ )
129
+
130
+ # Create element-scoped tempdir so that the ALFF and fALFF maps are
131
+ # available later as nibabel stores file path reference for
132
+ # loading on computation
133
+ element_tempdir = WorkDirManager().get_element_tempdir(
134
+ prefix="junifer_alff+falff"
135
+ )
136
+ output_alff_path = element_tempdir / "output_alff.nii.gz"
137
+ output_falff_path = element_tempdir / "output_falff.nii.gz"
138
+ # Save computed data to file
139
+ nib.save(alff_data, output_alff_path)
140
+ nib.save(falff_data, output_falff_path)
141
+
142
+ return alff_data, falff_data, output_alff_path, output_falff_path # type: ignore