junifer 0.0.4.dev493__py3-none-any.whl → 0.0.4.dev530__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.
@@ -6,11 +6,10 @@
6
6
 
7
7
 
8
8
  import hashlib
9
- import subprocess
10
9
  from functools import lru_cache
11
10
  from itertools import product
12
11
  from pathlib import Path
13
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast
12
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
14
13
 
15
14
  import nibabel as nib
16
15
  import numpy as np
@@ -20,7 +19,7 @@ from scipy.stats import rankdata
20
19
 
21
20
  from ...pipeline import WorkDirManager
22
21
  from ...pipeline.singleton import singleton
23
- from ...utils import logger, raise_error
22
+ from ...utils import logger, raise_error, run_ext_cmd
24
23
 
25
24
 
26
25
  if TYPE_CHECKING:
@@ -112,11 +111,6 @@ class ReHoEstimator:
112
111
  pathlib.Path
113
112
  The path to the ReHo map as NIfTI.
114
113
 
115
- Raises
116
- ------
117
- RuntimeError
118
- If the 3dReHo command fails due to some issue.
119
-
120
114
  Notes
121
115
  -----
122
116
  For more information on the publication, please check [1]_ , and for
@@ -174,30 +168,10 @@ class ReHoEstimator:
174
168
  else:
175
169
  reho_cmd.append(f"-nneigh {nneigh}")
176
170
  # Call 3dReHo
177
- reho_cmd_str = " ".join(reho_cmd)
178
- logger.info(f"3dReHo command to be executed: {reho_cmd_str}")
179
- reho_process = subprocess.run(
180
- reho_cmd_str, # string needed with shell=True
181
- stdin=subprocess.DEVNULL,
182
- stdout=subprocess.PIPE,
183
- stderr=subprocess.STDOUT,
184
- shell=True, # needed for respecting $PATH
185
- check=False,
186
- )
187
- if reho_process.returncode == 0:
188
- logger.info(
189
- "3dReHo succeeded with the following output: "
190
- f"{reho_process.stdout}"
191
- )
192
- else:
193
- raise_error(
194
- msg="3dReHo failed with the following error: "
195
- f"{reho_process.stdout}",
196
- klass=RuntimeError,
197
- )
171
+ run_ext_cmd(name="3dReHo", cmd=reho_cmd)
198
172
 
199
173
  # SHA256 for bypassing memmap
200
- sha256_params = hashlib.sha256(bytes(reho_cmd_str, "utf-8"))
174
+ sha256_params = hashlib.sha256(bytes(" ".join(reho_cmd), "utf-8"))
201
175
  # Create element-scoped tempdir so that the ReHo map is
202
176
  # available later as get_coordinates and the like need it
203
177
  # in ReHoSpheres and the like to transform to other template
@@ -216,27 +190,7 @@ class ReHoEstimator:
216
190
  f"{reho_afni_out_path_prefix}+tlrc.BRIK",
217
191
  ]
218
192
  # Call 3dAFNItoNIFTI
219
- convert_cmd_str = " ".join(convert_cmd)
220
- logger.info(f"3dAFNItoNIFTI command to be executed: {convert_cmd_str}")
221
- convert_process = subprocess.run(
222
- convert_cmd_str, # string needed with shell=True
223
- stdin=subprocess.DEVNULL,
224
- stdout=subprocess.PIPE,
225
- stderr=subprocess.STDOUT,
226
- shell=True, # needed for respecting $PATH
227
- check=False,
228
- )
229
- if convert_process.returncode == 0:
230
- logger.info(
231
- "3dAFNItoNIFTI succeeded with the following output: "
232
- f"{convert_process.stdout}"
233
- )
234
- else:
235
- raise_error(
236
- msg="3dAFNItoNIFTI failed with the following error: "
237
- f"{convert_process.stdout}",
238
- klass=RuntimeError,
239
- )
193
+ run_ext_cmd(name="3dAFNItoNIFTI", cmd=convert_cmd)
240
194
 
241
195
  # Cleanup intermediate files
242
196
  for fname in self.temp_dir_path.glob("reho*"): # type: ignore
@@ -244,9 +198,8 @@ class ReHoEstimator:
244
198
 
245
199
  # Load nifti
246
200
  output_data = nib.load(reho_afni_to_nifti_out_path)
247
- # Stupid casting
248
- output_data = cast("Nifti1Image", output_data)
249
- return output_data, reho_afni_to_nifti_out_path
201
+
202
+ return output_data, reho_afni_to_nifti_out_path # type: ignore
250
203
 
251
204
  def _compute_reho_python(
252
205
  self,
@@ -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,224 @@
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
+ from ..base import BasePreprocessor
24
+
25
+
26
+ if TYPE_CHECKING:
27
+ from nibabel import Nifti1Image
28
+
29
+
30
+ class _AntsApplyTransformsWarper(BasePreprocessor):
31
+ """Class for warping NIfTI images via ANTs antsApplyTransforms.
32
+
33
+ Warps ANTs ``antsApplyTransforms``.
34
+
35
+ Parameters
36
+ ----------
37
+ reference : str
38
+ The data type to use as reference for warping.
39
+ on : str
40
+ The data type to use for warping.
41
+
42
+ Raises
43
+ ------
44
+ ValueError
45
+ If a list was passed for ``on``.
46
+
47
+ """
48
+
49
+ _EXT_DEPENDENCIES: ClassVar[
50
+ List[Dict[str, Union[str, bool, List[str]]]]
51
+ ] = [
52
+ {
53
+ "name": "ants",
54
+ "optional": False,
55
+ "commands": ["ResampleImage", "antsApplyTransforms"],
56
+ },
57
+ ]
58
+
59
+ def __init__(self, reference: str, on: str) -> None:
60
+ """Initialize the class."""
61
+ self.ref = reference
62
+ # Check only single data type is passed
63
+ if isinstance(on, list):
64
+ raise_error("Can only work on single data type, list was passed.")
65
+ self.on = on # needed for the base validation to work
66
+ super().__init__(
67
+ on=self.on, required_data_types=[self.on, self.ref, "Warp"]
68
+ )
69
+
70
+ def get_valid_inputs(self) -> List[str]:
71
+ """Get valid data types for input.
72
+
73
+ Returns
74
+ -------
75
+ list of str
76
+ The list of data types that can be used as input for this
77
+ preprocessor.
78
+
79
+ """
80
+ # Constructed dynamically
81
+ return [self.on]
82
+
83
+ def get_output_type(self, input: List[str]) -> List[str]:
84
+ """Get output type.
85
+
86
+ Parameters
87
+ ----------
88
+ input : list of str
89
+ The input to the preprocessor. The list must contain the
90
+ available Junifer Data dictionary keys.
91
+
92
+ Returns
93
+ -------
94
+ list of str
95
+ The updated list of available Junifer Data object keys after
96
+ the pipeline step.
97
+
98
+ """
99
+ # Does not add any new keys
100
+ return input
101
+
102
+ def _run_apply_transforms(
103
+ self,
104
+ input_data: Dict,
105
+ ref_path: Path,
106
+ warp_path: Path,
107
+ ) -> Tuple["Nifti1Image", Path]:
108
+ """Run ``antsApplyTransforms``.
109
+
110
+ Parameters
111
+ ----------
112
+ input_data : dict
113
+ The input data.
114
+ ref_path : pathlib.Path
115
+ The path to the reference file.
116
+ warp_path : pathlib.Path
117
+ The path to the warp file.
118
+
119
+ Returns
120
+ -------
121
+ Niimg-like object
122
+ The warped input image.
123
+ pathlib.Path
124
+ The path to the resampled reference image.
125
+
126
+ """
127
+ # Get the min of the voxel sizes from input and use it as the
128
+ # resolution
129
+ resolution = np.min(input_data["data"].header.get_zooms()[:3])
130
+
131
+ # Create element-specific tempdir for storing post-warping assets
132
+ tempdir = WorkDirManager().get_element_tempdir(
133
+ prefix="applytransforms"
134
+ )
135
+
136
+ # Create a tempfile for resampled reference output
137
+ resample_image_out_path = tempdir / "reference_resampled.nii.gz"
138
+ # Set ResampleImage command
139
+ resample_image_cmd = [
140
+ "ResampleImage",
141
+ "3", # image dimension
142
+ f"{ref_path.resolve()}",
143
+ f"{resample_image_out_path.resolve()}",
144
+ f"{resolution}x{resolution}x{resolution}",
145
+ "0", # option for spacing and not size
146
+ "3 3", # Lanczos windowed sinc
147
+ ]
148
+ # Call ResampleImage
149
+ run_ext_cmd(name="ResampleImage", cmd=resample_image_cmd)
150
+
151
+ # Create a tempfile for warped output
152
+ apply_transforms_out_path = tempdir / "input_warped.nii.gz"
153
+ # Set antsApplyTransforms command
154
+ apply_transforms_cmd = [
155
+ "antsApplyTransforms",
156
+ "-d 3",
157
+ "-e 3",
158
+ "-n LanczosWindowedSinc",
159
+ f"-i {input_data['path'].resolve()}",
160
+ # use resampled reference
161
+ f"-r {resample_image_out_path.resolve()}",
162
+ f"-t {warp_path.resolve()}",
163
+ f"-o {apply_transforms_out_path.resolve()}",
164
+ ]
165
+ # Call antsApplyTransforms
166
+ run_ext_cmd(name="antsApplyTransforms", cmd=apply_transforms_cmd)
167
+
168
+ # Load nifti
169
+ output_img = nib.load(apply_transforms_out_path)
170
+
171
+ return output_img, resample_image_out_path # type: ignore
172
+
173
+ def preprocess(
174
+ self,
175
+ input: Dict[str, Any],
176
+ extra_input: Optional[Dict[str, Any]] = None,
177
+ ) -> Tuple[str, Dict[str, Any]]:
178
+ """Preprocess.
179
+
180
+ Parameters
181
+ ----------
182
+ input : dict
183
+ A single input from the Junifer Data object in which to preprocess.
184
+ extra_input : dict, optional
185
+ The other fields in the Junifer Data object. Must include the
186
+ ``Warp`` and ``ref`` value's keys.
187
+
188
+ Returns
189
+ -------
190
+ str
191
+ The key to store the output in the Junifer Data object.
192
+ dict
193
+ The computed result as dictionary. This will be stored in the
194
+ Junifer Data object under the key ``data`` of the data type.
195
+
196
+ Raises
197
+ ------
198
+ ValueError
199
+ If ``extra_input`` is None.
200
+
201
+ """
202
+ logger.debug("Warping via ANTs using antsApplyTransforms")
203
+ # Check for extra inputs
204
+ if extra_input is None:
205
+ raise_error(
206
+ f"No extra input provided, requires `Warp` and `{self.ref}` "
207
+ "data types in particular."
208
+ )
209
+ # Retrieve data type info to warp
210
+ to_warp_input = input
211
+ # Retrieve data type info to use as reference
212
+ ref_input = extra_input[self.ref]
213
+ # Retrieve Warp data
214
+ warp = extra_input["Warp"]
215
+ # Replace original data with warped data and add resampled reference
216
+ # path
217
+ input["data"], input["reference_path"] = self._run_apply_transforms(
218
+ input_data=to_warp_input,
219
+ ref_path=ref_input["path"],
220
+ warp_path=warp["path"],
221
+ )
222
+ # Use reference input's space as warped input's space
223
+ input["space"] = ref_input["space"]
224
+ return self.on, input
@@ -0,0 +1,124 @@
1
+ """Provide tests for AntsApplyTransformsWarper."""
2
+
3
+ # Authors: Synchon Mandal <s.mandal@fz-juelich.de>
4
+ # License: AGPL
5
+
6
+ import socket
7
+ from pathlib import Path
8
+ from typing import List
9
+
10
+ import nibabel as nib
11
+ import pytest
12
+
13
+ from junifer.datagrabber import DMCC13Benchmark
14
+ from junifer.datareader import DefaultDataReader
15
+ from junifer.pipeline.utils import _check_ants
16
+ from junifer.preprocess.ants.ants_apply_transforms_warper import (
17
+ _AntsApplyTransformsWarper,
18
+ )
19
+
20
+
21
+ def test_AntsApplyTransformsWarper_init() -> None:
22
+ """Test AntsApplyTransformsWarper init."""
23
+ ants_apply_transforms_warper = _AntsApplyTransformsWarper(
24
+ reference="T1w", on="BOLD"
25
+ )
26
+ assert ants_apply_transforms_warper.ref == "T1w"
27
+ assert ants_apply_transforms_warper.on == "BOLD"
28
+ assert ants_apply_transforms_warper._on == ["BOLD"]
29
+
30
+
31
+ def test_AntsApplyTransformsWarper_get_valid_inputs() -> None:
32
+ """Test AntsApplyTransformsWarper get_valid_inputs."""
33
+ ants_apply_transforms_warper = _AntsApplyTransformsWarper(
34
+ reference="T1w", on="BOLD"
35
+ )
36
+ assert ants_apply_transforms_warper.get_valid_inputs() == ["BOLD"]
37
+
38
+
39
+ @pytest.mark.parametrize(
40
+ "input_",
41
+ [
42
+ ["BOLD", "T1w", "Warp"],
43
+ ["BOLD", "T1w"],
44
+ ["BOLD"],
45
+ ],
46
+ )
47
+ def test_AntsApplyTransformsWarper_get_output_type(input_: List[str]) -> None:
48
+ """Test AntsApplyTransformsWarper get_output_type.
49
+
50
+ Parameters
51
+ ----------
52
+ input_ : list of str
53
+ The input data types.
54
+
55
+ """
56
+ ants_apply_transforms_warper = _AntsApplyTransformsWarper(
57
+ reference="T1w", on="BOLD"
58
+ )
59
+ assert ants_apply_transforms_warper.get_output_type(input_) == input_
60
+
61
+
62
+ @pytest.mark.skipif(
63
+ _check_ants() is False, reason="requires ANTs to be in PATH"
64
+ )
65
+ @pytest.mark.skipif(
66
+ socket.gethostname() != "juseless",
67
+ reason="only for juseless",
68
+ )
69
+ def test_AntsApplyTransformsWarper__run_apply_transform() -> None:
70
+ """Test AntsApplyTransformsWarper _run_apply_transform."""
71
+ with DMCC13Benchmark(
72
+ types=["BOLD", "T1w", "Warp"],
73
+ sessions=["wave1bas"],
74
+ tasks=["Rest"],
75
+ phase_encodings=["AP"],
76
+ runs=["1"],
77
+ native_t1w=True,
78
+ ) as dg:
79
+ # Read data
80
+ element_data = DefaultDataReader().fit_transform(
81
+ dg[("f9057kp", "wave1bas", "Rest", "AP", "1")]
82
+ )
83
+ # Preprocess data
84
+ warped_data, resampled_ref_path = _AntsApplyTransformsWarper(
85
+ reference="T1w", on="BOLD"
86
+ )._run_apply_transforms(
87
+ input_data=element_data["BOLD"],
88
+ ref_path=element_data["T1w"]["path"],
89
+ warp_path=element_data["Warp"]["path"],
90
+ )
91
+ assert isinstance(warped_data, nib.Nifti1Image)
92
+ assert isinstance(resampled_ref_path, Path)
93
+
94
+
95
+ @pytest.mark.skipif(
96
+ _check_ants() is False, reason="requires ANTs to be in PATH"
97
+ )
98
+ @pytest.mark.skipif(
99
+ socket.gethostname() != "juseless",
100
+ reason="only for juseless",
101
+ )
102
+ def test_AntsApplyTransformsWarper_preprocess() -> None:
103
+ """Test AntsApplyTransformsWarper preprocess."""
104
+ with DMCC13Benchmark(
105
+ types=["BOLD", "T1w", "Warp"],
106
+ sessions=["wave1bas"],
107
+ tasks=["Rest"],
108
+ phase_encodings=["AP"],
109
+ runs=["1"],
110
+ native_t1w=True,
111
+ ) as dg:
112
+ # Read data
113
+ element_data = DefaultDataReader().fit_transform(
114
+ dg[("f9057kp", "wave1bas", "Rest", "AP", "1")]
115
+ )
116
+ # Preprocess data
117
+ data_type, data = _AntsApplyTransformsWarper(
118
+ reference="T1w", on="BOLD"
119
+ ).preprocess(
120
+ input=element_data["BOLD"],
121
+ extra_input=element_data,
122
+ )
123
+ assert isinstance(data_type, str)
124
+ assert isinstance(data, dict)
@@ -15,6 +15,7 @@ from typing import (
15
15
 
16
16
  from ..api.decorators import register_preprocessor
17
17
  from ..utils import logger, raise_error
18
+ from .ants.ants_apply_transforms_warper import _AntsApplyTransformsWarper
18
19
  from .base import BasePreprocessor
19
20
  from .fsl.apply_warper import _ApplyWarper
20
21
 
@@ -35,8 +36,13 @@ class BOLDWarper(BasePreprocessor):
35
36
  ] = [
36
37
  {
37
38
  "name": "fsl",
38
- "optional": False,
39
- "commands": ["applywarp"],
39
+ "optional": True,
40
+ "commands": ["flirt", "applywarp"],
41
+ },
42
+ {
43
+ "name": "ants",
44
+ "optional": True,
45
+ "commands": ["ResampleImage", "antsApplyTransforms"],
40
46
  },
41
47
  ]
42
48
 
@@ -105,6 +111,8 @@ class BOLDWarper(BasePreprocessor):
105
111
  ------
106
112
  ValueError
107
113
  If ``extra_input`` is None.
114
+ RuntimeError
115
+ If warp / transformation file extension is not ".mat" or ".h5".
108
116
 
109
117
  """
110
118
  logger.debug("Warping BOLD using BOLDWarper")
@@ -114,11 +122,34 @@ class BOLDWarper(BasePreprocessor):
114
122
  f"No extra input provided, requires `Warp` and `{self.ref}` "
115
123
  "data types in particular."
116
124
  )
117
- # Initialize ApplyWarper for computation
118
- apply_warper = _ApplyWarper(reference=self.ref, on="BOLD")
119
- # Replace original BOLD data with warped BOLD data
120
- _, input = apply_warper.preprocess(
121
- input=input,
122
- extra_input=extra_input,
123
- )
125
+ # Check for warp file type to use correct tool
126
+ warp_file_ext = extra_input["Warp"]["path"].suffix
127
+ if warp_file_ext == ".mat":
128
+ logger.debug("Using FSL with BOLDWarper")
129
+ # Initialize ApplyWarper for computation
130
+ apply_warper = _ApplyWarper(reference=self.ref, on="BOLD")
131
+ # Replace original BOLD data with warped BOLD data
132
+ _, input = apply_warper.preprocess(
133
+ input=input,
134
+ extra_input=extra_input,
135
+ )
136
+ elif warp_file_ext == ".h5":
137
+ logger.debug("Using ANTs with BOLDWarper")
138
+ # Initialize AntsApplyTransformsWarper for computation
139
+ ants_apply_transforms_warper = _AntsApplyTransformsWarper(
140
+ reference=self.ref, on="BOLD"
141
+ )
142
+ # Replace original BOLD data with warped BOLD data
143
+ _, input = ants_apply_transforms_warper.preprocess(
144
+ input=input,
145
+ extra_input=extra_input,
146
+ )
147
+ else:
148
+ raise_error(
149
+ msg=(
150
+ "Unknown warp / transformation file extension: "
151
+ f"{warp_file_ext}"
152
+ ),
153
+ klass=RuntimeError,
154
+ )
124
155
  return "BOLD", input
@@ -3,7 +3,6 @@
3
3
  # Authors: Synchon Mandal <s.mandal@fz-juelich.de>
4
4
  # License: AGPL
5
5
 
6
- import subprocess
7
6
  from pathlib import Path
8
7
  from typing import (
9
8
  TYPE_CHECKING,
@@ -14,14 +13,13 @@ from typing import (
14
13
  Optional,
15
14
  Tuple,
16
15
  Union,
17
- cast,
18
16
  )
19
17
 
20
18
  import nibabel as nib
21
19
  import numpy as np
22
20
 
23
21
  from ...pipeline import WorkDirManager
24
- from ...utils import logger, raise_error
22
+ from ...utils import logger, raise_error, run_ext_cmd
25
23
  from ..base import BasePreprocessor
26
24
 
27
25
 
@@ -54,7 +52,7 @@ class _ApplyWarper(BasePreprocessor):
54
52
  {
55
53
  "name": "fsl",
56
54
  "optional": False,
57
- "commands": ["applywarp"],
55
+ "commands": ["flirt", "applywarp"],
58
56
  },
59
57
  ]
60
58
 
@@ -125,11 +123,6 @@ class _ApplyWarper(BasePreprocessor):
125
123
  pathlib.Path
126
124
  The path to the resampled reference image.
127
125
 
128
- Raises
129
- ------
130
- RuntimeError
131
- If FSL commands fail.
132
-
133
126
  """
134
127
  # Get the min of the voxel sizes from input and use it as the
135
128
  # resolution
@@ -150,29 +143,7 @@ class _ApplyWarper(BasePreprocessor):
150
143
  f"-out {flirt_out_path.resolve()}",
151
144
  ]
152
145
  # Call flirt
153
- flirt_cmd_str = " ".join(flirt_cmd)
154
- logger.info(f"flirt command to be executed: {flirt_cmd_str}")
155
- flirt_process = subprocess.run(
156
- flirt_cmd_str,
157
- stdin=subprocess.DEVNULL,
158
- stdout=subprocess.PIPE,
159
- stderr=subprocess.STDOUT,
160
- shell=True, # needed for respecting $PATH
161
- check=False,
162
- )
163
- if flirt_process.returncode == 0:
164
- logger.info(
165
- "flirt succeeded with the following output: "
166
- f"{flirt_process.stdout}"
167
- )
168
- else:
169
- raise_error(
170
- msg="flirt failed with the following error: "
171
- f"{flirt_process.stdout}",
172
- klass=RuntimeError,
173
- )
174
-
175
- # TODO(synchon): Modify reference or not?
146
+ run_ext_cmd(name="flirt", cmd=flirt_cmd)
176
147
 
177
148
  # Create a tempfile for warped output
178
149
  applywarp_out_path = tempdir / "input_warped.nii.gz"
@@ -186,34 +157,12 @@ class _ApplyWarper(BasePreprocessor):
186
157
  f"-o {applywarp_out_path.resolve()}",
187
158
  ]
188
159
  # Call applywarp
189
- applywarp_cmd_str = " ".join(applywarp_cmd)
190
- logger.info(f"applywarp command to be executed: {applywarp_cmd_str}")
191
- applywarp_process = subprocess.run(
192
- applywarp_cmd_str, # string needed with shell=True
193
- stdin=subprocess.DEVNULL,
194
- stdout=subprocess.PIPE,
195
- stderr=subprocess.STDOUT,
196
- shell=True, # needed for respecting $PATH
197
- check=False,
198
- )
199
- if applywarp_process.returncode == 0:
200
- logger.info(
201
- "applywarp succeeded with the following output: "
202
- f"{applywarp_process.stdout}"
203
- )
204
- else:
205
- raise_error(
206
- msg="applywarp failed with the following error: "
207
- f"{applywarp_process.stdout}",
208
- klass=RuntimeError,
209
- )
160
+ run_ext_cmd(name="applywarp", cmd=applywarp_cmd)
210
161
 
211
162
  # Load nifti
212
163
  output_img = nib.load(applywarp_out_path)
213
164
 
214
- # Stupid casting
215
- output_img = cast("Nifti1Image", output_img)
216
- return output_img, flirt_out_path
165
+ return output_img, flirt_out_path # type: ignore
217
166
 
218
167
  def preprocess(
219
168
  self,