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,258 @@
1
+ """Define concrete class for generating GNU Parallel (local) assets."""
2
+
3
+ # Authors: Synchon Mandal <s.mandal@fz-juelich.de>
4
+ # License: AGPL
5
+
6
+ import shutil
7
+ import textwrap
8
+ from pathlib import Path
9
+ from typing import Dict, List, Optional, Tuple, Union
10
+
11
+ from ...utils import logger, make_executable, raise_error, run_ext_cmd
12
+ from .queue_context_adapter import QueueContextAdapter
13
+
14
+
15
+ __all__ = ["GnuParallelLocalAdapter"]
16
+
17
+
18
+ class GnuParallelLocalAdapter(QueueContextAdapter):
19
+ """Class for generating commands for GNU Parallel (local).
20
+
21
+ Parameters
22
+ ----------
23
+ job_name : str
24
+ The job name.
25
+ job_dir : pathlib.Path
26
+ The path to the job directory.
27
+ yaml_config_path : pathlib.Path
28
+ The path to the YAML config file.
29
+ elements : list of str or tuple
30
+ Element(s) to process. Will be used to index the DataGrabber.
31
+ pre_run : str or None, optional
32
+ Extra shell commands to source before the run (default None).
33
+ pre_collect : str or None, optional
34
+ Extra bash commands to source before the collect (default None).
35
+ env : dict, optional
36
+ The Python environment configuration. If None, will run without a
37
+ virtual environment of any kind (default None).
38
+ verbose : str, optional
39
+ The level of verbosity (default "info").
40
+ submit : bool, optional
41
+ Whether to submit the jobs (default False).
42
+
43
+ Raises
44
+ ------
45
+ ValueError
46
+ If``env`` is invalid.
47
+
48
+ See Also
49
+ --------
50
+ QueueContextAdapter :
51
+ The base class for QueueContext.
52
+ HTCondorAdapter :
53
+ The concrete class for queueing via HTCondor.
54
+
55
+ """
56
+
57
+ def __init__(
58
+ self,
59
+ job_name: str,
60
+ job_dir: Path,
61
+ yaml_config_path: Path,
62
+ elements: List[Union[str, Tuple]],
63
+ pre_run: Optional[str] = None,
64
+ pre_collect: Optional[str] = None,
65
+ env: Optional[Dict[str, str]] = None,
66
+ verbose: str = "info",
67
+ submit: bool = False,
68
+ ) -> None:
69
+ """Initialize the class."""
70
+ self._job_name = job_name
71
+ self._job_dir = job_dir
72
+ self._yaml_config_path = yaml_config_path
73
+ self._elements = elements
74
+ self._pre_run = pre_run
75
+ self._pre_collect = pre_collect
76
+ self._check_env(env)
77
+ self._verbose = verbose
78
+ self._submit = submit
79
+
80
+ self._log_dir = self._job_dir / "logs"
81
+ self._pre_run_path = self._job_dir / "pre_run.sh"
82
+ self._pre_collect_path = self._job_dir / "pre_collect.sh"
83
+ self._run_path = self._job_dir / f"run_{self._job_name}.sh"
84
+ self._collect_path = self._job_dir / f"collect_{self._job_name}.sh"
85
+ self._run_joblog_path = self._job_dir / f"run_{self._job_name}_joblog"
86
+ self._elements_file_path = self._job_dir / "elements"
87
+
88
+ def _check_env(self, env: Optional[Dict[str, str]]) -> None:
89
+ """Check value of env parameter on init.
90
+
91
+ Parameters
92
+ ----------
93
+ env : dict or None
94
+ The value of env parameter.
95
+
96
+ Raises
97
+ ------
98
+ ValueError
99
+ If ``env.kind`` is invalid.
100
+
101
+ """
102
+ # Set env related variables
103
+ if env is None:
104
+ env = {"kind": "local"}
105
+ # Check env kind
106
+ valid_env_kinds = ["conda", "venv", "local"]
107
+ if env["kind"] not in valid_env_kinds:
108
+ raise_error(
109
+ f"Invalid value for `env.kind`: {env['kind']}, "
110
+ f"must be one of {valid_env_kinds}"
111
+ )
112
+ else:
113
+ # Set variables
114
+ if env["kind"] == "local":
115
+ # No virtual environment
116
+ self._executable = "junifer"
117
+ self._arguments = ""
118
+ else:
119
+ self._executable = f"run_{env['kind']}.sh"
120
+ self._arguments = f"{env['name']} junifer"
121
+ self._exec_path = self._job_dir / self._executable
122
+
123
+ def elements(self) -> str:
124
+ """Return elements to run."""
125
+ elements_to_run = []
126
+ for element in self._elements:
127
+ # Stringify elements if tuple for operation
128
+ str_element = (
129
+ ",".join(element) if isinstance(element, tuple) else element
130
+ )
131
+ elements_to_run.append(str_element)
132
+
133
+ return "\n".join(elements_to_run)
134
+
135
+ def pre_run(self) -> str:
136
+ """Return pre-run commands."""
137
+ fixed = (
138
+ "#!/usr/bin/env bash\n\n"
139
+ "# This script is auto-generated by junifer.\n\n"
140
+ "# Force datalad to run in non-interactive mode\n"
141
+ "DATALAD_UI_INTERACTIVE=false\n"
142
+ )
143
+ var = self._pre_run or ""
144
+ return fixed + "\n" + var
145
+
146
+ def run(self) -> str:
147
+ """Return run commands."""
148
+ return (
149
+ "#!/usr/bin/env bash\n\n"
150
+ "# This script is auto-generated by junifer.\n\n"
151
+ "# Run pre_run.sh\n"
152
+ f"sh {self._pre_run_path.resolve()!s}\n\n"
153
+ "# Run `junifer run` using `parallel`\n"
154
+ "parallel --bar --resume --resume-failed "
155
+ f"--joblog {self._run_joblog_path} "
156
+ "--delay 60 " # wait 1 min before next job is spawned
157
+ f"--results {self._log_dir} "
158
+ f"--arg-file {self._elements_file_path.resolve()!s} "
159
+ f"{self._job_dir.resolve()!s}/{self._executable} "
160
+ f"{self._arguments} run "
161
+ f"{self._yaml_config_path.resolve()!s} "
162
+ f"--verbose {self._verbose} "
163
+ f"--element"
164
+ )
165
+
166
+ def pre_collect(self) -> str:
167
+ """Return pre-collect commands."""
168
+ fixed = (
169
+ "#!/usr/bin/env bash\n\n"
170
+ "# This script is auto-generated by junifer.\n"
171
+ )
172
+ var = self._pre_collect or ""
173
+ return fixed + "\n" + var
174
+
175
+ def collect(self) -> str:
176
+ """Return collect commands."""
177
+ return (
178
+ "#!/usr/bin/env bash\n\n"
179
+ "# This script is auto-generated by junifer.\n\n"
180
+ "# Run pre_collect.sh\n"
181
+ f"sh {self._pre_collect_path.resolve()!s}\n\n"
182
+ "# Run `junifer collect`\n"
183
+ f"{self._job_dir.resolve()!s}/{self._executable} "
184
+ f"{self._arguments} collect "
185
+ f"{self._yaml_config_path.resolve()!s} "
186
+ f"--verbose {self._verbose}"
187
+ )
188
+
189
+ def prepare(self) -> None:
190
+ """Prepare assets for submission."""
191
+ logger.info("Preparing for local queue via GNU parallel")
192
+ # Copy executable if not local
193
+ if hasattr(self, "_exec_path"):
194
+ logger.info(
195
+ f"Copying {self._executable} to "
196
+ f"{self._exec_path.resolve()!s}"
197
+ )
198
+ shutil.copy(
199
+ src=Path(__file__).parent.parent / "res" / self._executable,
200
+ dst=self._exec_path,
201
+ )
202
+ make_executable(self._exec_path)
203
+ # Create elements file
204
+ logger.info(
205
+ f"Writing {self._elements_file_path.name} to "
206
+ f"{self._elements_file_path.resolve()!s}"
207
+ )
208
+ self._elements_file_path.touch()
209
+ self._elements_file_path.write_text(textwrap.dedent(self.elements()))
210
+ # Create pre run
211
+ logger.info(
212
+ f"Writing {self._pre_run_path.name} to "
213
+ f"{self._job_dir.resolve()!s}"
214
+ )
215
+ self._pre_run_path.touch()
216
+ self._pre_run_path.write_text(textwrap.dedent(self.pre_run()))
217
+ make_executable(self._pre_run_path)
218
+ # Create run
219
+ logger.info(
220
+ f"Writing {self._run_path.name} to " f"{self._job_dir.resolve()!s}"
221
+ )
222
+ self._run_path.touch()
223
+ self._run_path.write_text(textwrap.dedent(self.run()))
224
+ make_executable(self._run_path)
225
+ # Create pre collect
226
+ logger.info(
227
+ f"Writing {self._pre_collect_path.name} to "
228
+ f"{self._job_dir.resolve()!s}"
229
+ )
230
+ self._pre_collect_path.touch()
231
+ self._pre_collect_path.write_text(textwrap.dedent(self.pre_collect()))
232
+ make_executable(self._pre_collect_path)
233
+ # Create collect
234
+ logger.info(
235
+ f"Writing {self._collect_path.name} to "
236
+ f"{self._job_dir.resolve()!s}"
237
+ )
238
+ self._collect_path.touch()
239
+ self._collect_path.write_text(textwrap.dedent(self.collect()))
240
+ make_executable(self._collect_path)
241
+ # Submit if required
242
+ run_cmd = f"sh {self._run_path.resolve()!s}"
243
+ collect_cmd = f"sh {self._collect_path.resolve()!s}"
244
+ if self._submit:
245
+ logger.info(
246
+ "Shell scripts created, the following will be run:\n"
247
+ f"{run_cmd}\n"
248
+ "After successful completion of the previous step, run:\n"
249
+ f"{collect_cmd}"
250
+ )
251
+ run_ext_cmd(name=f"{self._run_path.resolve()!s}", cmd=[run_cmd])
252
+ else:
253
+ logger.info(
254
+ "Shell scripts created, to start, run:\n"
255
+ f"{run_cmd}\n"
256
+ "After successful completion of the previous step, run:\n"
257
+ f"{collect_cmd}"
258
+ )
@@ -0,0 +1,365 @@
1
+ """Define concrete class for generating HTCondor assets."""
2
+
3
+ # Authors: Synchon Mandal <s.mandal@fz-juelich.de>
4
+ # License: AGPL
5
+
6
+ import shutil
7
+ import textwrap
8
+ from pathlib import Path
9
+ from typing import Dict, List, Optional, Tuple, Union
10
+
11
+ from ...utils import logger, make_executable, raise_error, run_ext_cmd
12
+ from .queue_context_adapter import QueueContextAdapter
13
+
14
+
15
+ __all__ = ["HTCondorAdapter"]
16
+
17
+
18
+ class HTCondorAdapter(QueueContextAdapter):
19
+ """Class for generating queueing scripts for HTCondor.
20
+
21
+ Parameters
22
+ ----------
23
+ job_name : str
24
+ The job name to be used by HTCondor.
25
+ job_dir : pathlib.Path
26
+ The path to the job directory.
27
+ yaml_config_path : pathlib.Path
28
+ The path to the YAML config file.
29
+ elements : list of str or tuple
30
+ Element(s) to process. Will be used to index the DataGrabber.
31
+ pre_run : str or None, optional
32
+ Extra bash commands to source before the run (default None).
33
+ pre_collect : str or None, optional
34
+ Extra bash commands to source before the collect (default None).
35
+ env : dict, optional
36
+ The Python environment configuration. If None, will run without a
37
+ virtual environment of any kind (default None).
38
+ verbose : str, optional
39
+ The level of verbosity (default "info").
40
+ cpus : int, optional
41
+ The number of CPU cores to use (default 1).
42
+ mem : str, optional
43
+ The size of memory (RAM) to use (default "8G").
44
+ disk : str, optional
45
+ The size of disk (HDD or SSD) to use (default "1G").
46
+ extra_preamble : str or None, optional
47
+ Extra commands to pass to HTCondor (default None).
48
+ collect : {"yes", "on_success_only", "no"}, optional
49
+ Whether to submit "collect" task for junifer (default "yes").
50
+ Valid options are:
51
+
52
+ * "yes": Submit "collect" task and run even if some of the jobs
53
+ fail.
54
+ * "on_success_only": Submit "collect" task and run only if all jobs
55
+ succeed.
56
+ * "no": Do not submit "collect" task.
57
+
58
+ submit : bool, optional
59
+ Whether to submit the jobs. In any case, .dag files will be created
60
+ for submission (default False).
61
+
62
+ Raises
63
+ ------
64
+ ValueError
65
+ If ``collect`` is invalid or if ``env`` is invalid.
66
+
67
+ See Also
68
+ --------
69
+ QueueContextAdapter :
70
+ The base class for QueueContext.
71
+ GnuParallelLocalAdapter :
72
+ The concrete class for queueing via GNU Parallel (local).
73
+
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ job_name: str,
79
+ job_dir: Path,
80
+ yaml_config_path: Path,
81
+ elements: List[Union[str, Tuple]],
82
+ pre_run: Optional[str] = None,
83
+ pre_collect: Optional[str] = None,
84
+ env: Optional[Dict[str, str]] = None,
85
+ verbose: str = "info",
86
+ cpus: int = 1,
87
+ mem: str = "8G",
88
+ disk: str = "1G",
89
+ extra_preamble: Optional[str] = None,
90
+ collect: str = "yes",
91
+ submit: bool = False,
92
+ ) -> None:
93
+ """Initialize the class."""
94
+ self._job_name = job_name
95
+ self._job_dir = job_dir
96
+ self._yaml_config_path = yaml_config_path
97
+ self._elements = elements
98
+ self._pre_run = pre_run
99
+ self._pre_collect = pre_collect
100
+ self._check_env(env)
101
+ self._verbose = verbose
102
+ self._cpus = cpus
103
+ self._mem = mem
104
+ self._disk = disk
105
+ self._extra_preamble = extra_preamble
106
+ self._collect = self._check_collect(collect)
107
+ self._submit = submit
108
+
109
+ self._log_dir = self._job_dir / "logs"
110
+ self._pre_run_path = self._job_dir / "pre_run.sh"
111
+ self._pre_collect_path = self._job_dir / "pre_collect.sh"
112
+ self._submit_run_path = self._job_dir / f"run_{self._job_name}.submit"
113
+ self._submit_collect_path = (
114
+ self._job_dir / f"collect_{self._job_name}.submit"
115
+ )
116
+ self._dag_path = self._job_dir / f"{self._job_name}.dag"
117
+
118
+ def _check_env(self, env: Optional[Dict[str, str]]) -> None:
119
+ """Check value of env parameter on init.
120
+
121
+ Parameters
122
+ ----------
123
+ env : dict or None
124
+ The value of env parameter.
125
+
126
+ Raises
127
+ ------
128
+ ValueError
129
+ If ``env.kind`` is invalid.
130
+
131
+ """
132
+ # Set env related variables
133
+ if env is None:
134
+ env = {"kind": "local"}
135
+ # Check env kind
136
+ valid_env_kinds = ["conda", "venv", "local"]
137
+ if env["kind"] not in valid_env_kinds:
138
+ raise_error(
139
+ f"Invalid value for `env.kind`: {env['kind']}, "
140
+ f"must be one of {valid_env_kinds}"
141
+ )
142
+ else:
143
+ # Set variables
144
+ if env["kind"] == "local":
145
+ # No virtual environment
146
+ self._executable = "junifer"
147
+ self._arguments = ""
148
+ else:
149
+ self._executable = f"run_{env['kind']}.sh"
150
+ self._arguments = f"{env['name']} junifer"
151
+ self._exec_path = self._job_dir / self._executable
152
+
153
+ def _check_collect(self, collect: str) -> str:
154
+ """Check value of collect parameter on init.
155
+
156
+ Parameters
157
+ ----------
158
+ collect : str
159
+ The value of collect parameter.
160
+
161
+ Returns
162
+ -------
163
+ str
164
+ The checked value of collect parameter.
165
+
166
+ Raises
167
+ ------
168
+ ValueError
169
+ If ``collect`` is invalid.
170
+
171
+ """
172
+ valid_options = ["yes", "no", "on_success_only"]
173
+ if collect not in valid_options:
174
+ raise_error(
175
+ f"Invalid value for `collect`: {collect}, "
176
+ f"must be one of {valid_options}"
177
+ )
178
+ else:
179
+ return collect
180
+
181
+ def pre_run(self) -> str:
182
+ """Return pre-run commands."""
183
+ fixed = (
184
+ "#!/bin/bash\n\n"
185
+ "# This script is auto-generated by junifer.\n\n"
186
+ "# Force datalad to run in non-interactive mode\n"
187
+ "DATALAD_UI_INTERACTIVE=false\n"
188
+ )
189
+ var = self._pre_run or ""
190
+ return fixed + "\n" + var
191
+
192
+ def run(self) -> str:
193
+ """Return run commands."""
194
+ junifer_run_args = (
195
+ "run "
196
+ f"{self._yaml_config_path.resolve()!s} "
197
+ f"--verbose {self._verbose} "
198
+ "--element $(element)"
199
+ )
200
+ log_dir_prefix = (
201
+ f"{self._log_dir.resolve()!s}/junifer_run_$(log_element)"
202
+ )
203
+ fixed = (
204
+ "# This script is auto-generated by junifer.\n\n"
205
+ "# Environment\n"
206
+ "universe = vanilla\n"
207
+ "getenv = True\n\n"
208
+ "# Resources\n"
209
+ f"request_cpus = {self._cpus}\n"
210
+ f"request_memory = {self._mem}\n"
211
+ f"request_disk = {self._disk}\n\n"
212
+ "# Executable\n"
213
+ f"initial_dir = {self._job_dir.resolve()!s}\n"
214
+ f"executable = $(initial_dir)/{self._executable}\n"
215
+ f"transfer_executable = False\n\n"
216
+ f"arguments = {self._arguments} {junifer_run_args}\n\n"
217
+ "# Logs\n"
218
+ f"log = {log_dir_prefix}.log\n"
219
+ f"output = {log_dir_prefix}.out\n"
220
+ f"error = {log_dir_prefix}.err\n"
221
+ )
222
+ var = self._extra_preamble or ""
223
+ return fixed + "\n" + var + "\n" + "queue"
224
+
225
+ def pre_collect(self) -> str:
226
+ """Return pre-collect commands."""
227
+ fixed = (
228
+ "#!/bin/bash\n\n" "# This script is auto-generated by junifer.\n"
229
+ )
230
+ var = self._pre_collect or ""
231
+ # Add commands if collect="yes"
232
+ if self._collect == "yes":
233
+ var += 'if [ "${1}" == "4" ]; then\n' " exit 1\n" "fi\n"
234
+ return fixed + "\n" + var
235
+
236
+ def collect(self) -> str:
237
+ """Return collect commands."""
238
+ junifer_collect_args = (
239
+ "collect "
240
+ f"{self._yaml_config_path.resolve()!s} "
241
+ f"--verbose {self._verbose}"
242
+ )
243
+ log_dir_prefix = f"{self._log_dir.resolve()!s}/junifer_collect"
244
+ fixed = (
245
+ "# This script is auto-generated by junifer.\n\n"
246
+ "# Environment\n"
247
+ "universe = vanilla\n"
248
+ "getenv = True\n\n"
249
+ "# Resources\n"
250
+ f"request_cpus = {self._cpus}\n"
251
+ f"request_memory = {self._mem}\n"
252
+ f"request_disk = {self._disk}\n\n"
253
+ "# Executable\n"
254
+ f"initial_dir = {self._job_dir.resolve()!s}\n"
255
+ f"executable = $(initial_dir)/{self._executable}\n"
256
+ "transfer_executable = False\n\n"
257
+ f"arguments = {self._arguments} {junifer_collect_args}\n\n"
258
+ "# Logs\n"
259
+ f"log = {log_dir_prefix}.log\n"
260
+ f"output = {log_dir_prefix}.out\n"
261
+ f"error = {log_dir_prefix}.err\n"
262
+ )
263
+ var = self._extra_preamble or ""
264
+ return fixed + "\n" + var + "\n" + "queue"
265
+
266
+ def dag(self) -> str:
267
+ """Return HTCondor DAG commands."""
268
+ fixed = ""
269
+ for idx, element in enumerate(self._elements):
270
+ # Stringify elements if tuple for operation
271
+ str_element = (
272
+ ",".join(element) if isinstance(element, tuple) else element
273
+ )
274
+ # Stringify elements if tuple for logging
275
+ log_element = (
276
+ "-".join(element) if isinstance(element, tuple) else element
277
+ )
278
+ fixed += (
279
+ f"JOB run{idx} {self._submit_run_path}\n"
280
+ f'VARS run{idx} element="{str_element}" ' # needs to be
281
+ f'log_element="{log_element}"\n\n' # double quoted
282
+ )
283
+ var = ""
284
+ if self._collect == "yes":
285
+ var += (
286
+ f"FINAL collect {self._submit_collect_path}\n"
287
+ f"SCRIPT PRE collect {self._pre_collect_path.as_posix()} "
288
+ "$DAG_STATUS\n"
289
+ )
290
+ elif self._collect == "on_success_only":
291
+ var += f"JOB collect {self._submit_collect_path}\n" "PARENT "
292
+ for idx, _ in enumerate(self._elements):
293
+ var += f"run{idx} "
294
+ var += "CHILD collect\n"
295
+
296
+ return fixed + "\n" + var
297
+
298
+ def prepare(self) -> None:
299
+ """Prepare assets for submission."""
300
+ logger.info("Creating HTCondor job")
301
+ # Create logs
302
+ logger.info(
303
+ f"Creating logs directory under " f"{self._job_dir.resolve()!s}"
304
+ )
305
+ self._log_dir.mkdir(exist_ok=True, parents=True)
306
+ # Copy executable if not local
307
+ if hasattr(self, "_exec_path"):
308
+ logger.info(
309
+ f"Copying {self._executable} to "
310
+ f"{self._exec_path.resolve()!s}"
311
+ )
312
+ shutil.copy(
313
+ src=Path(__file__).parent.parent / "res" / self._executable,
314
+ dst=self._exec_path,
315
+ )
316
+ make_executable(self._exec_path)
317
+ # Create pre run
318
+ logger.info(
319
+ f"Writing {self._pre_run_path.name} to "
320
+ f"{self._job_dir.resolve()!s}"
321
+ )
322
+ self._pre_run_path.touch()
323
+ self._pre_run_path.write_text(textwrap.dedent(self.pre_run()))
324
+ make_executable(self._pre_run_path)
325
+ # Create run
326
+ logger.debug(
327
+ f"Writing {self._submit_run_path.name} to "
328
+ f"{self._job_dir.resolve()!s}"
329
+ )
330
+ self._submit_run_path.touch()
331
+ self._submit_run_path.write_text(textwrap.dedent(self.run()))
332
+ # Create pre collect
333
+ logger.info(
334
+ f"Writing {self._pre_collect_path.name} to "
335
+ f"{self._job_dir.resolve()!s}"
336
+ )
337
+ self._pre_collect_path.touch()
338
+ self._pre_collect_path.write_text(textwrap.dedent(self.pre_collect()))
339
+ make_executable(self._pre_collect_path)
340
+ # Create collect
341
+ logger.debug(
342
+ f"Writing {self._submit_collect_path.name} to "
343
+ f"{self._job_dir.resolve()!s}"
344
+ )
345
+ self._submit_collect_path.touch()
346
+ self._submit_collect_path.write_text(textwrap.dedent(self.collect()))
347
+ # Create DAG
348
+ logger.debug(
349
+ f"Writing {self._dag_path.name} to " f"{self._job_dir.resolve()!s}"
350
+ )
351
+ self._dag_path.touch()
352
+ self._dag_path.write_text(textwrap.dedent(self.dag()))
353
+ # Submit if required
354
+ condor_submit_dag_cmd = [
355
+ "condor_submit_dag",
356
+ "-include_env HOME",
357
+ f"{self._dag_path.resolve()!s}",
358
+ ]
359
+ if self._submit:
360
+ run_ext_cmd(name="condor_submit_dag", cmd=condor_submit_dag_cmd)
361
+ else:
362
+ logger.info(
363
+ f"HTCondor job files created, to submit the job, run:\n"
364
+ f"{' '.join(condor_submit_dag_cmd)}"
365
+ )
@@ -0,0 +1,60 @@
1
+ """Define abstract base class for queue context adapter."""
2
+
3
+ # Authors: Synchon Mandal <s.mandal@fz-juelich.de>
4
+ # License: AGPL
5
+
6
+ from abc import ABC, abstractmethod
7
+
8
+ from ...utils import raise_error
9
+
10
+
11
+ __all__ = ["QueueContextAdapter"]
12
+
13
+
14
+ class QueueContextAdapter(ABC):
15
+ """Abstract base class for queue context adapter.
16
+
17
+ For every interface that is required, one needs to provide a concrete
18
+ implementation of this abstract class.
19
+
20
+ """
21
+
22
+ @abstractmethod
23
+ def pre_run(self) -> str:
24
+ """Return pre-run commands."""
25
+ raise_error(
26
+ msg="Concrete classes need to implement pre_run()",
27
+ klass=NotImplementedError,
28
+ )
29
+
30
+ @abstractmethod
31
+ def run(self) -> str:
32
+ """Return run commands."""
33
+ raise_error(
34
+ msg="Concrete classes need to implement run()",
35
+ klass=NotImplementedError,
36
+ )
37
+
38
+ @abstractmethod
39
+ def pre_collect(self) -> str:
40
+ """Return pre-collect commands."""
41
+ raise_error(
42
+ msg="Concrete classes need to implement pre_collect()",
43
+ klass=NotImplementedError,
44
+ )
45
+
46
+ @abstractmethod
47
+ def collect(self) -> str:
48
+ """Return collect commands."""
49
+ raise_error(
50
+ msg="Concrete classes need to implement collect()",
51
+ klass=NotImplementedError,
52
+ )
53
+
54
+ @abstractmethod
55
+ def prepare(self) -> None:
56
+ """Prepare assets for submission."""
57
+ raise_error(
58
+ msg="Concrete classes need to implement prepare()",
59
+ klass=NotImplementedError,
60
+ )