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.
- junifer/_version.py +14 -2
- junifer/api/cli.py +162 -17
- junifer/api/functions.py +87 -419
- junifer/api/parser.py +24 -0
- junifer/api/queue_context/__init__.py +8 -0
- junifer/api/queue_context/gnu_parallel_local_adapter.py +258 -0
- junifer/api/queue_context/htcondor_adapter.py +365 -0
- junifer/api/queue_context/queue_context_adapter.py +60 -0
- junifer/api/queue_context/tests/test_gnu_parallel_local_adapter.py +192 -0
- junifer/api/queue_context/tests/test_htcondor_adapter.py +257 -0
- junifer/api/res/afni/run_afni_docker.sh +6 -6
- junifer/api/res/ants/ResampleImage +3 -0
- junifer/api/res/ants/antsApplyTransforms +3 -0
- junifer/api/res/ants/antsApplyTransformsToPoints +3 -0
- junifer/api/res/ants/run_ants_docker.sh +39 -0
- junifer/api/res/fsl/applywarp +3 -0
- junifer/api/res/fsl/flirt +3 -0
- junifer/api/res/fsl/img2imgcoord +3 -0
- junifer/api/res/fsl/run_fsl_docker.sh +39 -0
- junifer/api/res/fsl/std2imgcoord +3 -0
- junifer/api/res/run_conda.sh +4 -4
- junifer/api/res/run_venv.sh +22 -0
- junifer/api/tests/data/partly_cloudy_agg_mean_tian.yml +16 -0
- junifer/api/tests/test_api_utils.py +21 -3
- junifer/api/tests/test_cli.py +232 -9
- junifer/api/tests/test_functions.py +211 -439
- junifer/api/tests/test_parser.py +1 -1
- junifer/configs/juseless/datagrabbers/aomic_id1000_vbm.py +6 -1
- junifer/configs/juseless/datagrabbers/camcan_vbm.py +6 -1
- junifer/configs/juseless/datagrabbers/ixi_vbm.py +6 -1
- junifer/configs/juseless/datagrabbers/tests/test_ucla.py +8 -8
- junifer/configs/juseless/datagrabbers/ucla.py +44 -26
- junifer/configs/juseless/datagrabbers/ukb_vbm.py +6 -1
- junifer/data/VOIs/meta/AutobiographicalMemory_VOIs.txt +23 -0
- junifer/data/VOIs/meta/Power2013_MNI_VOIs.tsv +264 -0
- junifer/data/__init__.py +4 -0
- junifer/data/coordinates.py +298 -31
- junifer/data/masks.py +360 -28
- junifer/data/parcellations.py +621 -188
- junifer/data/template_spaces.py +190 -0
- junifer/data/tests/test_coordinates.py +34 -3
- junifer/data/tests/test_data_utils.py +1 -0
- junifer/data/tests/test_masks.py +202 -86
- junifer/data/tests/test_parcellations.py +266 -55
- junifer/data/tests/test_template_spaces.py +104 -0
- junifer/data/utils.py +4 -2
- junifer/datagrabber/__init__.py +1 -0
- junifer/datagrabber/aomic/id1000.py +111 -70
- junifer/datagrabber/aomic/piop1.py +116 -53
- junifer/datagrabber/aomic/piop2.py +116 -53
- junifer/datagrabber/aomic/tests/test_id1000.py +27 -27
- junifer/datagrabber/aomic/tests/test_piop1.py +27 -27
- junifer/datagrabber/aomic/tests/test_piop2.py +27 -27
- junifer/datagrabber/base.py +62 -10
- junifer/datagrabber/datalad_base.py +0 -2
- junifer/datagrabber/dmcc13_benchmark.py +372 -0
- junifer/datagrabber/hcp1200/datalad_hcp1200.py +5 -0
- junifer/datagrabber/hcp1200/hcp1200.py +30 -13
- junifer/datagrabber/pattern.py +133 -27
- junifer/datagrabber/pattern_datalad.py +111 -13
- junifer/datagrabber/tests/test_base.py +57 -6
- junifer/datagrabber/tests/test_datagrabber_utils.py +204 -76
- junifer/datagrabber/tests/test_datalad_base.py +0 -6
- junifer/datagrabber/tests/test_dmcc13_benchmark.py +256 -0
- junifer/datagrabber/tests/test_multiple.py +43 -10
- junifer/datagrabber/tests/test_pattern.py +125 -178
- junifer/datagrabber/tests/test_pattern_datalad.py +44 -25
- junifer/datagrabber/utils.py +151 -16
- junifer/datareader/default.py +36 -10
- junifer/external/nilearn/junifer_nifti_spheres_masker.py +6 -0
- junifer/markers/base.py +25 -16
- junifer/markers/collection.py +35 -16
- junifer/markers/complexity/__init__.py +27 -0
- junifer/markers/complexity/complexity_base.py +149 -0
- junifer/markers/complexity/hurst_exponent.py +136 -0
- junifer/markers/complexity/multiscale_entropy_auc.py +140 -0
- junifer/markers/complexity/perm_entropy.py +132 -0
- junifer/markers/complexity/range_entropy.py +136 -0
- junifer/markers/complexity/range_entropy_auc.py +145 -0
- junifer/markers/complexity/sample_entropy.py +134 -0
- junifer/markers/complexity/tests/test_complexity_base.py +19 -0
- junifer/markers/complexity/tests/test_hurst_exponent.py +69 -0
- junifer/markers/complexity/tests/test_multiscale_entropy_auc.py +68 -0
- junifer/markers/complexity/tests/test_perm_entropy.py +68 -0
- junifer/markers/complexity/tests/test_range_entropy.py +69 -0
- junifer/markers/complexity/tests/test_range_entropy_auc.py +69 -0
- junifer/markers/complexity/tests/test_sample_entropy.py +68 -0
- junifer/markers/complexity/tests/test_weighted_perm_entropy.py +68 -0
- junifer/markers/complexity/weighted_perm_entropy.py +133 -0
- junifer/markers/falff/_afni_falff.py +153 -0
- junifer/markers/falff/_junifer_falff.py +142 -0
- junifer/markers/falff/falff_base.py +91 -84
- junifer/markers/falff/falff_parcels.py +61 -45
- junifer/markers/falff/falff_spheres.py +64 -48
- junifer/markers/falff/tests/test_falff_parcels.py +89 -121
- junifer/markers/falff/tests/test_falff_spheres.py +92 -127
- junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py +1 -0
- junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py +1 -0
- junifer/markers/functional_connectivity/functional_connectivity_base.py +1 -0
- junifer/markers/functional_connectivity/tests/test_crossparcellation_functional_connectivity.py +46 -44
- junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_parcels.py +34 -39
- junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_spheres.py +40 -52
- junifer/markers/functional_connectivity/tests/test_functional_connectivity_parcels.py +62 -70
- junifer/markers/functional_connectivity/tests/test_functional_connectivity_spheres.py +99 -85
- junifer/markers/parcel_aggregation.py +60 -38
- junifer/markers/reho/_afni_reho.py +192 -0
- junifer/markers/reho/_junifer_reho.py +281 -0
- junifer/markers/reho/reho_base.py +69 -34
- junifer/markers/reho/reho_parcels.py +26 -16
- junifer/markers/reho/reho_spheres.py +23 -9
- junifer/markers/reho/tests/test_reho_parcels.py +93 -92
- junifer/markers/reho/tests/test_reho_spheres.py +88 -86
- junifer/markers/sphere_aggregation.py +54 -9
- junifer/markers/temporal_snr/temporal_snr_base.py +1 -0
- junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py +38 -37
- junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py +34 -38
- junifer/markers/tests/test_collection.py +43 -42
- junifer/markers/tests/test_ets_rss.py +29 -37
- junifer/markers/tests/test_parcel_aggregation.py +587 -468
- junifer/markers/tests/test_sphere_aggregation.py +209 -157
- junifer/markers/utils.py +2 -40
- junifer/onthefly/read_transform.py +13 -6
- junifer/pipeline/__init__.py +1 -0
- junifer/pipeline/pipeline_step_mixin.py +105 -41
- junifer/pipeline/registry.py +17 -0
- junifer/pipeline/singleton.py +45 -0
- junifer/pipeline/tests/test_pipeline_step_mixin.py +139 -51
- junifer/pipeline/tests/test_update_meta_mixin.py +1 -0
- junifer/pipeline/tests/test_workdir_manager.py +104 -0
- junifer/pipeline/update_meta_mixin.py +8 -2
- junifer/pipeline/utils.py +154 -15
- junifer/pipeline/workdir_manager.py +246 -0
- junifer/preprocess/__init__.py +3 -0
- junifer/preprocess/ants/__init__.py +4 -0
- junifer/preprocess/ants/ants_apply_transforms_warper.py +185 -0
- junifer/preprocess/ants/tests/test_ants_apply_transforms_warper.py +56 -0
- junifer/preprocess/base.py +96 -69
- junifer/preprocess/bold_warper.py +265 -0
- junifer/preprocess/confounds/fmriprep_confound_remover.py +91 -134
- junifer/preprocess/confounds/tests/test_fmriprep_confound_remover.py +106 -111
- junifer/preprocess/fsl/__init__.py +4 -0
- junifer/preprocess/fsl/apply_warper.py +179 -0
- junifer/preprocess/fsl/tests/test_apply_warper.py +45 -0
- junifer/preprocess/tests/test_bold_warper.py +159 -0
- junifer/preprocess/tests/test_preprocess_base.py +6 -6
- junifer/preprocess/warping/__init__.py +6 -0
- junifer/preprocess/warping/_ants_warper.py +167 -0
- junifer/preprocess/warping/_fsl_warper.py +109 -0
- junifer/preprocess/warping/space_warper.py +213 -0
- junifer/preprocess/warping/tests/test_space_warper.py +198 -0
- junifer/stats.py +18 -4
- junifer/storage/base.py +9 -1
- junifer/storage/hdf5.py +8 -3
- junifer/storage/pandas_base.py +2 -1
- junifer/storage/sqlite.py +1 -0
- junifer/storage/tests/test_hdf5.py +2 -1
- junifer/storage/tests/test_sqlite.py +8 -8
- junifer/storage/tests/test_utils.py +6 -6
- junifer/storage/utils.py +1 -0
- junifer/testing/datagrabbers.py +11 -7
- junifer/testing/utils.py +1 -0
- junifer/tests/test_stats.py +2 -0
- junifer/utils/__init__.py +1 -0
- junifer/utils/helpers.py +53 -0
- junifer/utils/logging.py +14 -3
- junifer/utils/tests/test_helpers.py +35 -0
- {junifer-0.0.3.dev186.dist-info → junifer-0.0.4.dist-info}/METADATA +59 -28
- junifer-0.0.4.dist-info/RECORD +257 -0
- {junifer-0.0.3.dev186.dist-info → junifer-0.0.4.dist-info}/WHEEL +1 -1
- junifer/markers/falff/falff_estimator.py +0 -334
- junifer/markers/falff/tests/test_falff_estimator.py +0 -238
- junifer/markers/reho/reho_estimator.py +0 -515
- junifer/markers/reho/tests/test_reho_estimator.py +0 -260
- junifer-0.0.3.dev186.dist-info/RECORD +0 -199
- {junifer-0.0.3.dev186.dist-info → junifer-0.0.4.dist-info}/AUTHORS.rst +0 -0
- {junifer-0.0.3.dev186.dist-info → junifer-0.0.4.dist-info}/LICENSE.md +0 -0
- {junifer-0.0.3.dev186.dist-info → junifer-0.0.4.dist-info}/entry_points.txt +0 -0
- {junifer-0.0.3.dev186.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
|
+
)
|