hpcflow 0.1.15__py3-none-any.whl → 0.2.0a271__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.
- hpcflow/__init__.py +2 -11
- hpcflow/__pyinstaller/__init__.py +5 -0
- hpcflow/__pyinstaller/hook-hpcflow.py +40 -0
- hpcflow/_version.py +1 -1
- hpcflow/app.py +43 -0
- hpcflow/cli.py +2 -461
- hpcflow/data/demo_data_manifest/__init__.py +3 -0
- hpcflow/data/demo_data_manifest/demo_data_manifest.json +6 -0
- hpcflow/data/jinja_templates/test/test_template.txt +8 -0
- hpcflow/data/programs/hello_world/README.md +1 -0
- hpcflow/data/programs/hello_world/hello_world.c +87 -0
- hpcflow/data/programs/hello_world/linux/hello_world +0 -0
- hpcflow/data/programs/hello_world/macos/hello_world +0 -0
- hpcflow/data/programs/hello_world/win/hello_world.exe +0 -0
- hpcflow/data/scripts/__init__.py +1 -0
- hpcflow/data/scripts/bad_script.py +2 -0
- hpcflow/data/scripts/demo_task_1_generate_t1_infile_1.py +8 -0
- hpcflow/data/scripts/demo_task_1_generate_t1_infile_2.py +8 -0
- hpcflow/data/scripts/demo_task_1_parse_p3.py +7 -0
- hpcflow/data/scripts/do_nothing.py +2 -0
- hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
- hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/generate_t1_file_01.py +7 -0
- hpcflow/data/scripts/import_future_script.py +7 -0
- hpcflow/data/scripts/input_file_generator_basic.py +3 -0
- hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
- hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_all_iters_test.py +15 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_env_spec.py +7 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_labels.py +8 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_sub_param_in_direct_out.py +6 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj_group.py +12 -0
- hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +11 -0
- hpcflow/data/scripts/main_script_test_json_and_direct_in_json_out.py +14 -0
- hpcflow/data/scripts/main_script_test_json_in_json_and_direct_out.py +17 -0
- hpcflow/data/scripts/main_script_test_json_in_json_out.py +14 -0
- hpcflow/data/scripts/main_script_test_json_in_json_out_labels.py +16 -0
- hpcflow/data/scripts/main_script_test_json_in_obj.py +12 -0
- hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
- hpcflow/data/scripts/main_script_test_json_out_obj.py +10 -0
- hpcflow/data/scripts/main_script_test_json_sub_param_in_json_out_labels.py +16 -0
- hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
- hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
- hpcflow/data/scripts/output_file_parser_basic.py +3 -0
- hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
- hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/parse_t1_file_01.py +4 -0
- hpcflow/data/scripts/script_exit_test.py +5 -0
- hpcflow/data/template_components/__init__.py +1 -0
- hpcflow/data/template_components/command_files.yaml +26 -0
- hpcflow/data/template_components/environments.yaml +13 -0
- hpcflow/data/template_components/parameters.yaml +14 -0
- hpcflow/data/template_components/task_schemas.yaml +139 -0
- hpcflow/data/workflows/workflow_1.yaml +5 -0
- hpcflow/examples.ipynb +1037 -0
- hpcflow/sdk/__init__.py +149 -0
- hpcflow/sdk/app.py +4266 -0
- hpcflow/sdk/cli.py +1479 -0
- hpcflow/sdk/cli_common.py +385 -0
- hpcflow/sdk/config/__init__.py +5 -0
- hpcflow/sdk/config/callbacks.py +246 -0
- hpcflow/sdk/config/cli.py +388 -0
- hpcflow/sdk/config/config.py +1410 -0
- hpcflow/sdk/config/config_file.py +501 -0
- hpcflow/sdk/config/errors.py +272 -0
- hpcflow/sdk/config/types.py +150 -0
- hpcflow/sdk/core/__init__.py +38 -0
- hpcflow/sdk/core/actions.py +3857 -0
- hpcflow/sdk/core/app_aware.py +25 -0
- hpcflow/sdk/core/cache.py +224 -0
- hpcflow/sdk/core/command_files.py +814 -0
- hpcflow/sdk/core/commands.py +424 -0
- hpcflow/sdk/core/element.py +2071 -0
- hpcflow/sdk/core/enums.py +221 -0
- hpcflow/sdk/core/environment.py +256 -0
- hpcflow/sdk/core/errors.py +1043 -0
- hpcflow/sdk/core/execute.py +207 -0
- hpcflow/sdk/core/json_like.py +809 -0
- hpcflow/sdk/core/loop.py +1320 -0
- hpcflow/sdk/core/loop_cache.py +282 -0
- hpcflow/sdk/core/object_list.py +933 -0
- hpcflow/sdk/core/parameters.py +3371 -0
- hpcflow/sdk/core/rule.py +196 -0
- hpcflow/sdk/core/run_dir_files.py +57 -0
- hpcflow/sdk/core/skip_reason.py +7 -0
- hpcflow/sdk/core/task.py +3792 -0
- hpcflow/sdk/core/task_schema.py +993 -0
- hpcflow/sdk/core/test_utils.py +538 -0
- hpcflow/sdk/core/types.py +447 -0
- hpcflow/sdk/core/utils.py +1207 -0
- hpcflow/sdk/core/validation.py +87 -0
- hpcflow/sdk/core/values.py +477 -0
- hpcflow/sdk/core/workflow.py +4820 -0
- hpcflow/sdk/core/zarr_io.py +206 -0
- hpcflow/sdk/data/__init__.py +13 -0
- hpcflow/sdk/data/config_file_schema.yaml +34 -0
- hpcflow/sdk/data/config_schema.yaml +260 -0
- hpcflow/sdk/data/environments_spec_schema.yaml +21 -0
- hpcflow/sdk/data/files_spec_schema.yaml +5 -0
- hpcflow/sdk/data/parameters_spec_schema.yaml +7 -0
- hpcflow/sdk/data/task_schema_spec_schema.yaml +3 -0
- hpcflow/sdk/data/workflow_spec_schema.yaml +22 -0
- hpcflow/sdk/demo/__init__.py +3 -0
- hpcflow/sdk/demo/cli.py +242 -0
- hpcflow/sdk/helper/__init__.py +3 -0
- hpcflow/sdk/helper/cli.py +137 -0
- hpcflow/sdk/helper/helper.py +300 -0
- hpcflow/sdk/helper/watcher.py +192 -0
- hpcflow/sdk/log.py +288 -0
- hpcflow/sdk/persistence/__init__.py +18 -0
- hpcflow/sdk/persistence/base.py +2817 -0
- hpcflow/sdk/persistence/defaults.py +6 -0
- hpcflow/sdk/persistence/discovery.py +39 -0
- hpcflow/sdk/persistence/json.py +954 -0
- hpcflow/sdk/persistence/pending.py +948 -0
- hpcflow/sdk/persistence/store_resource.py +203 -0
- hpcflow/sdk/persistence/types.py +309 -0
- hpcflow/sdk/persistence/utils.py +73 -0
- hpcflow/sdk/persistence/zarr.py +2388 -0
- hpcflow/sdk/runtime.py +320 -0
- hpcflow/sdk/submission/__init__.py +3 -0
- hpcflow/sdk/submission/enums.py +70 -0
- hpcflow/sdk/submission/jobscript.py +2379 -0
- hpcflow/sdk/submission/schedulers/__init__.py +281 -0
- hpcflow/sdk/submission/schedulers/direct.py +233 -0
- hpcflow/sdk/submission/schedulers/sge.py +376 -0
- hpcflow/sdk/submission/schedulers/slurm.py +598 -0
- hpcflow/sdk/submission/schedulers/utils.py +25 -0
- hpcflow/sdk/submission/shells/__init__.py +52 -0
- hpcflow/sdk/submission/shells/base.py +229 -0
- hpcflow/sdk/submission/shells/bash.py +504 -0
- hpcflow/sdk/submission/shells/os_version.py +115 -0
- hpcflow/sdk/submission/shells/powershell.py +352 -0
- hpcflow/sdk/submission/submission.py +1402 -0
- hpcflow/sdk/submission/types.py +140 -0
- hpcflow/sdk/typing.py +194 -0
- hpcflow/sdk/utils/arrays.py +69 -0
- hpcflow/sdk/utils/deferred_file.py +55 -0
- hpcflow/sdk/utils/hashing.py +16 -0
- hpcflow/sdk/utils/patches.py +31 -0
- hpcflow/sdk/utils/strings.py +69 -0
- hpcflow/tests/api/test_api.py +32 -0
- hpcflow/tests/conftest.py +123 -0
- hpcflow/tests/data/__init__.py +0 -0
- hpcflow/tests/data/benchmark_N_elements.yaml +6 -0
- hpcflow/tests/data/benchmark_script_runner.yaml +26 -0
- hpcflow/tests/data/multi_path_sequences.yaml +29 -0
- hpcflow/tests/data/workflow_1.json +10 -0
- hpcflow/tests/data/workflow_1.yaml +5 -0
- hpcflow/tests/data/workflow_1_slurm.yaml +8 -0
- hpcflow/tests/data/workflow_1_wsl.yaml +8 -0
- hpcflow/tests/data/workflow_test_run_abort.yaml +42 -0
- hpcflow/tests/jinja_templates/test_jinja_templates.py +161 -0
- hpcflow/tests/programs/test_programs.py +180 -0
- hpcflow/tests/schedulers/direct_linux/test_direct_linux_submission.py +12 -0
- hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
- hpcflow/tests/schedulers/slurm/test_slurm_submission.py +14 -0
- hpcflow/tests/scripts/test_input_file_generators.py +282 -0
- hpcflow/tests/scripts/test_main_scripts.py +1361 -0
- hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
- hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
- hpcflow/tests/shells/wsl/test_wsl_submission.py +14 -0
- hpcflow/tests/unit/test_action.py +1066 -0
- hpcflow/tests/unit/test_action_rule.py +24 -0
- hpcflow/tests/unit/test_app.py +132 -0
- hpcflow/tests/unit/test_cache.py +46 -0
- hpcflow/tests/unit/test_cli.py +172 -0
- hpcflow/tests/unit/test_command.py +377 -0
- hpcflow/tests/unit/test_config.py +195 -0
- hpcflow/tests/unit/test_config_file.py +162 -0
- hpcflow/tests/unit/test_element.py +666 -0
- hpcflow/tests/unit/test_element_iteration.py +88 -0
- hpcflow/tests/unit/test_element_set.py +158 -0
- hpcflow/tests/unit/test_group.py +115 -0
- hpcflow/tests/unit/test_input_source.py +1479 -0
- hpcflow/tests/unit/test_input_value.py +398 -0
- hpcflow/tests/unit/test_jobscript_unit.py +757 -0
- hpcflow/tests/unit/test_json_like.py +1247 -0
- hpcflow/tests/unit/test_loop.py +2674 -0
- hpcflow/tests/unit/test_meta_task.py +325 -0
- hpcflow/tests/unit/test_multi_path_sequences.py +259 -0
- hpcflow/tests/unit/test_object_list.py +116 -0
- hpcflow/tests/unit/test_parameter.py +243 -0
- hpcflow/tests/unit/test_persistence.py +664 -0
- hpcflow/tests/unit/test_resources.py +243 -0
- hpcflow/tests/unit/test_run.py +286 -0
- hpcflow/tests/unit/test_run_directories.py +29 -0
- hpcflow/tests/unit/test_runtime.py +9 -0
- hpcflow/tests/unit/test_schema_input.py +372 -0
- hpcflow/tests/unit/test_shell.py +129 -0
- hpcflow/tests/unit/test_slurm.py +39 -0
- hpcflow/tests/unit/test_submission.py +502 -0
- hpcflow/tests/unit/test_task.py +2560 -0
- hpcflow/tests/unit/test_task_schema.py +182 -0
- hpcflow/tests/unit/test_utils.py +616 -0
- hpcflow/tests/unit/test_value_sequence.py +549 -0
- hpcflow/tests/unit/test_values.py +91 -0
- hpcflow/tests/unit/test_workflow.py +827 -0
- hpcflow/tests/unit/test_workflow_template.py +186 -0
- hpcflow/tests/unit/utils/test_arrays.py +40 -0
- hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
- hpcflow/tests/unit/utils/test_hashing.py +65 -0
- hpcflow/tests/unit/utils/test_patches.py +5 -0
- hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
- hpcflow/tests/unit/utils/test_strings.py +97 -0
- hpcflow/tests/workflows/__init__.py +0 -0
- hpcflow/tests/workflows/test_directory_structure.py +31 -0
- hpcflow/tests/workflows/test_jobscript.py +355 -0
- hpcflow/tests/workflows/test_run_status.py +198 -0
- hpcflow/tests/workflows/test_skip_downstream.py +696 -0
- hpcflow/tests/workflows/test_submission.py +140 -0
- hpcflow/tests/workflows/test_workflows.py +564 -0
- hpcflow/tests/workflows/test_zip.py +18 -0
- hpcflow/viz_demo.ipynb +6794 -0
- hpcflow-0.2.0a271.dist-info/LICENSE +375 -0
- hpcflow-0.2.0a271.dist-info/METADATA +65 -0
- hpcflow-0.2.0a271.dist-info/RECORD +237 -0
- {hpcflow-0.1.15.dist-info → hpcflow-0.2.0a271.dist-info}/WHEEL +4 -5
- hpcflow-0.2.0a271.dist-info/entry_points.txt +6 -0
- hpcflow/api.py +0 -490
- hpcflow/archive/archive.py +0 -307
- hpcflow/archive/cloud/cloud.py +0 -45
- hpcflow/archive/cloud/errors.py +0 -9
- hpcflow/archive/cloud/providers/dropbox.py +0 -427
- hpcflow/archive/errors.py +0 -5
- hpcflow/base_db.py +0 -4
- hpcflow/config.py +0 -233
- hpcflow/copytree.py +0 -66
- hpcflow/data/examples/_config.yml +0 -14
- hpcflow/data/examples/damask/demo/1.run.yml +0 -4
- hpcflow/data/examples/damask/demo/2.process.yml +0 -29
- hpcflow/data/examples/damask/demo/geom.geom +0 -2052
- hpcflow/data/examples/damask/demo/load.load +0 -1
- hpcflow/data/examples/damask/demo/material.config +0 -185
- hpcflow/data/examples/damask/inputs/geom.geom +0 -2052
- hpcflow/data/examples/damask/inputs/load.load +0 -1
- hpcflow/data/examples/damask/inputs/material.config +0 -185
- hpcflow/data/examples/damask/profiles/_variable_lookup.yml +0 -21
- hpcflow/data/examples/damask/profiles/damask.yml +0 -4
- hpcflow/data/examples/damask/profiles/damask_process.yml +0 -8
- hpcflow/data/examples/damask/profiles/damask_run.yml +0 -5
- hpcflow/data/examples/damask/profiles/default.yml +0 -6
- hpcflow/data/examples/thinking.yml +0 -177
- hpcflow/errors.py +0 -2
- hpcflow/init_db.py +0 -37
- hpcflow/models.py +0 -2595
- hpcflow/nesting.py +0 -9
- hpcflow/profiles.py +0 -455
- hpcflow/project.py +0 -81
- hpcflow/scheduler.py +0 -322
- hpcflow/utils.py +0 -103
- hpcflow/validation.py +0 -166
- hpcflow/variables.py +0 -543
- hpcflow-0.1.15.dist-info/METADATA +0 -168
- hpcflow-0.1.15.dist-info/RECORD +0 -45
- hpcflow-0.1.15.dist-info/entry_points.txt +0 -8
- hpcflow-0.1.15.dist-info/top_level.txt +0 -1
- /hpcflow/{archive → data/jinja_templates}/__init__.py +0 -0
- /hpcflow/{archive/cloud → data/programs}/__init__.py +0 -0
- /hpcflow/{archive/cloud/providers → data/workflows}/__init__.py +0 -0
|
@@ -0,0 +1,2379 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Model of information submitted to a scheduler.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import socket
|
|
11
|
+
import subprocess
|
|
12
|
+
from textwrap import dedent, indent
|
|
13
|
+
from typing import TextIO, cast, overload, TYPE_CHECKING
|
|
14
|
+
from typing_extensions import override
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from hpcflow.sdk.core import SKIPPED_EXIT_CODE
|
|
18
|
+
from hpcflow.sdk.core.enums import EARStatus
|
|
19
|
+
from hpcflow.sdk.core.errors import (
|
|
20
|
+
JobscriptSubmissionFailure,
|
|
21
|
+
NotSubmitMachineError,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from hpcflow.sdk.typing import hydrate
|
|
25
|
+
from hpcflow.sdk.core.json_like import ChildObjectSpec, JSONLike
|
|
26
|
+
from hpcflow.sdk.core.utils import nth_value, parse_timestamp, current_timestamp
|
|
27
|
+
from hpcflow.sdk.utils.strings import extract_py_from_future_imports
|
|
28
|
+
from hpcflow.sdk.log import TimeIt
|
|
29
|
+
from hpcflow.sdk.submission.schedulers import QueuedScheduler
|
|
30
|
+
from hpcflow.sdk.submission.schedulers.direct import DirectScheduler
|
|
31
|
+
from hpcflow.sdk.submission.shells import get_shell, DEFAULT_SHELL_NAMES
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
|
35
|
+
from datetime import datetime
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any, ClassVar, Literal
|
|
38
|
+
from typing_extensions import TypeIs
|
|
39
|
+
from numpy.typing import NDArray, ArrayLike
|
|
40
|
+
from ..core.actions import ElementActionRun
|
|
41
|
+
from ..core.element import ElementResources
|
|
42
|
+
from ..core.loop_cache import LoopIndex
|
|
43
|
+
from ..core.types import JobscriptSubmissionFailureArgs, BlockActionKey
|
|
44
|
+
from ..core.workflow import WorkflowTask, Workflow
|
|
45
|
+
from ..persistence.base import PersistentStore
|
|
46
|
+
from .submission import Submission
|
|
47
|
+
from .shells.base import Shell
|
|
48
|
+
from .schedulers import Scheduler
|
|
49
|
+
from .enums import JobscriptElementState
|
|
50
|
+
from .types import (
|
|
51
|
+
JobScriptCreationArguments,
|
|
52
|
+
JobScriptDescriptor,
|
|
53
|
+
ResolvedJobscriptBlockDependencies,
|
|
54
|
+
SchedulerRef,
|
|
55
|
+
VersionInfo,
|
|
56
|
+
)
|
|
57
|
+
from ..core.cache import ObjectCache
|
|
58
|
+
from hpcflow.sdk.submission.submission import JOBSCRIPT_SUBMIT_TIME_KEYS
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_jobscript_array(
|
|
62
|
+
resources: ElementResources, num_elements: int, store: PersistentStore
|
|
63
|
+
) -> bool:
|
|
64
|
+
"""Return True if a job array should be used for the specified `ElementResources`."""
|
|
65
|
+
if resources.scheduler in ("direct", "direct_posix"):
|
|
66
|
+
if resources.use_job_array:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"`use_job_array` not supported by scheduler: {resources.scheduler!r}"
|
|
69
|
+
)
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
if resources.combine_scripts:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
run_parallelism = store._features.EAR_parallelism
|
|
76
|
+
if resources.use_job_array is None:
|
|
77
|
+
if num_elements > 1 and run_parallelism:
|
|
78
|
+
return True
|
|
79
|
+
else:
|
|
80
|
+
return False
|
|
81
|
+
else:
|
|
82
|
+
if resources.use_job_array and not run_parallelism:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"Store type {store!r} does not support element parallelism, so jobs "
|
|
85
|
+
f"cannot be submitted as scheduler arrays."
|
|
86
|
+
)
|
|
87
|
+
return resources.use_job_array
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@TimeIt.decorator
|
|
91
|
+
def generate_EAR_resource_map(
|
|
92
|
+
task: WorkflowTask,
|
|
93
|
+
loop_idx: LoopIndex[str, int],
|
|
94
|
+
cache: ObjectCache,
|
|
95
|
+
) -> tuple[Sequence[ElementResources], Sequence[int], NDArray, NDArray]:
|
|
96
|
+
"""
|
|
97
|
+
Generate an integer array whose rows represent actions and columns represent task
|
|
98
|
+
elements and whose values index unique resources.
|
|
99
|
+
"""
|
|
100
|
+
none_val = -1
|
|
101
|
+
resources: list[ElementResources] = []
|
|
102
|
+
resource_hashes: list[int] = []
|
|
103
|
+
|
|
104
|
+
arr_shape = (task.num_actions, task.num_elements)
|
|
105
|
+
resource_map = np.empty(arr_shape, dtype=int)
|
|
106
|
+
EAR_ID_map = np.empty(arr_shape, dtype=int)
|
|
107
|
+
resource_map[:] = none_val
|
|
108
|
+
EAR_ID_map[:] = none_val
|
|
109
|
+
|
|
110
|
+
assert cache.elements is not None
|
|
111
|
+
assert cache.iterations is not None
|
|
112
|
+
|
|
113
|
+
for elem_id in task.element_IDs:
|
|
114
|
+
element = cache.elements[elem_id]
|
|
115
|
+
for iter_ID_i in element.iteration_IDs:
|
|
116
|
+
iter_i = cache.iterations[iter_ID_i]
|
|
117
|
+
if iter_i.loop_idx != loop_idx:
|
|
118
|
+
continue
|
|
119
|
+
if iter_i.EARs_initialised: # not strictly needed (actions will be empty)
|
|
120
|
+
for act_idx, action in iter_i.actions.items():
|
|
121
|
+
for run in action.runs:
|
|
122
|
+
if run.status == EARStatus.pending:
|
|
123
|
+
# TODO: consider `time_limit`s
|
|
124
|
+
res_hash = run.resources.get_jobscript_hash()
|
|
125
|
+
if res_hash not in resource_hashes:
|
|
126
|
+
resource_hashes.append(res_hash)
|
|
127
|
+
resources.append(run.resources)
|
|
128
|
+
resource_map[act_idx][element.index] = resource_hashes.index(
|
|
129
|
+
res_hash
|
|
130
|
+
)
|
|
131
|
+
EAR_ID_map[act_idx, element.index] = run.id_
|
|
132
|
+
|
|
133
|
+
# set defaults for and validate unique resources:
|
|
134
|
+
for res in resources:
|
|
135
|
+
res.set_defaults()
|
|
136
|
+
res.validate_against_machine()
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
resources,
|
|
140
|
+
resource_hashes,
|
|
141
|
+
resource_map,
|
|
142
|
+
EAR_ID_map,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@TimeIt.decorator
|
|
147
|
+
def group_resource_map_into_jobscripts(
|
|
148
|
+
resource_map: ArrayLike,
|
|
149
|
+
none_val: Any = -1,
|
|
150
|
+
) -> tuple[list[JobScriptDescriptor], NDArray]:
|
|
151
|
+
"""
|
|
152
|
+
Convert a resource map into a plan for what elements to group together into jobscripts.
|
|
153
|
+
"""
|
|
154
|
+
resource_map_ = np.asanyarray(resource_map)
|
|
155
|
+
resource_idx = np.unique(resource_map_)
|
|
156
|
+
jobscripts: list[JobScriptDescriptor] = []
|
|
157
|
+
allocated = np.zeros_like(resource_map_)
|
|
158
|
+
js_map = np.ones_like(resource_map_, dtype=float) * np.nan
|
|
159
|
+
nones_bool: NDArray = resource_map_ == none_val
|
|
160
|
+
stop = False
|
|
161
|
+
for act_idx in range(resource_map_.shape[0]):
|
|
162
|
+
for res_i in resource_idx:
|
|
163
|
+
if res_i == none_val:
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
if res_i not in resource_map_[act_idx]:
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
resource_map_[nones_bool] = res_i
|
|
170
|
+
diff = np.cumsum(np.abs(np.diff(resource_map_[act_idx:], axis=0)), axis=0)
|
|
171
|
+
|
|
172
|
+
elem_bool = np.logical_and(
|
|
173
|
+
resource_map_[act_idx] == res_i, allocated[act_idx] == False
|
|
174
|
+
)
|
|
175
|
+
elem_idx = np.where(elem_bool)[0]
|
|
176
|
+
act_elem_bool = np.logical_and(elem_bool, nones_bool[act_idx] == False)
|
|
177
|
+
act_elem_idx: tuple[NDArray, ...] = np.where(act_elem_bool)
|
|
178
|
+
|
|
179
|
+
# add elements from downstream actions:
|
|
180
|
+
ds_bool = np.logical_and(
|
|
181
|
+
diff[:, elem_idx] == 0,
|
|
182
|
+
nones_bool[act_idx + 1 :, elem_idx] == False,
|
|
183
|
+
)
|
|
184
|
+
ds_act_idx: NDArray
|
|
185
|
+
ds_elem_idx: NDArray
|
|
186
|
+
ds_act_idx, ds_elem_idx = np.where(ds_bool)
|
|
187
|
+
ds_act_idx += act_idx + 1
|
|
188
|
+
ds_elem_idx = elem_idx[ds_elem_idx]
|
|
189
|
+
|
|
190
|
+
EARs_by_elem: dict[int, list[int]] = {
|
|
191
|
+
k.item(): [act_idx] for k in act_elem_idx[0]
|
|
192
|
+
}
|
|
193
|
+
for ds_a, ds_e in zip(ds_act_idx, ds_elem_idx):
|
|
194
|
+
EARs_by_elem.setdefault(ds_e.item(), []).append(ds_a.item())
|
|
195
|
+
|
|
196
|
+
EARs = np.vstack([np.ones_like(act_elem_idx) * act_idx, act_elem_idx])
|
|
197
|
+
EARs = np.hstack([EARs, np.array([ds_act_idx, ds_elem_idx])])
|
|
198
|
+
|
|
199
|
+
if not EARs.size:
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
js: JobScriptDescriptor = {
|
|
203
|
+
"resources": res_i,
|
|
204
|
+
"elements": dict(sorted(EARs_by_elem.items(), key=lambda x: x[0])),
|
|
205
|
+
}
|
|
206
|
+
allocated[EARs[0], EARs[1]] = True
|
|
207
|
+
js_map[EARs[0], EARs[1]] = len(jobscripts)
|
|
208
|
+
jobscripts.append(js)
|
|
209
|
+
|
|
210
|
+
if np.all(allocated[~nones_bool]):
|
|
211
|
+
stop = True
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
if stop:
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
resource_map_[nones_bool] = none_val
|
|
218
|
+
|
|
219
|
+
return jobscripts, js_map
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@TimeIt.decorator
|
|
223
|
+
def resolve_jobscript_dependencies(
|
|
224
|
+
jobscripts: Mapping[int, JobScriptCreationArguments],
|
|
225
|
+
element_deps: Mapping[int, Mapping[int, Sequence[int]]],
|
|
226
|
+
) -> Mapping[int, dict[int, ResolvedJobscriptBlockDependencies]]:
|
|
227
|
+
"""
|
|
228
|
+
Discover concrete dependencies between jobscripts.
|
|
229
|
+
"""
|
|
230
|
+
# first pass is to find the mappings between jobscript elements:
|
|
231
|
+
jobscript_deps: dict[int, dict[int, ResolvedJobscriptBlockDependencies]] = {}
|
|
232
|
+
for js_idx, elem_deps in element_deps.items():
|
|
233
|
+
# keys of new dict are other jobscript indices on which this jobscript (js_idx)
|
|
234
|
+
# depends:
|
|
235
|
+
jobscript_deps[js_idx] = {}
|
|
236
|
+
|
|
237
|
+
for js_elem_idx_i, EAR_deps_i in elem_deps.items():
|
|
238
|
+
# locate which jobscript elements this jobscript element depends on:
|
|
239
|
+
for EAR_dep_j in EAR_deps_i:
|
|
240
|
+
for js_k_idx, js_k in jobscripts.items():
|
|
241
|
+
if js_k_idx == js_idx:
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
if EAR_dep_j in js_k["EAR_ID"]:
|
|
245
|
+
if js_k_idx not in jobscript_deps[js_idx]:
|
|
246
|
+
jobscript_deps[js_idx][js_k_idx] = {"js_element_mapping": {}}
|
|
247
|
+
|
|
248
|
+
jobscript_deps[js_idx][js_k_idx]["js_element_mapping"].setdefault(
|
|
249
|
+
js_elem_idx_i, []
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# retrieve column index, which is the JS-element index:
|
|
253
|
+
js_elem_idx_k: int = np.where(
|
|
254
|
+
np.any(js_k["EAR_ID"] == EAR_dep_j, axis=0)
|
|
255
|
+
)[0][0].item()
|
|
256
|
+
|
|
257
|
+
# add js dependency element-mapping:
|
|
258
|
+
if (
|
|
259
|
+
js_elem_idx_k
|
|
260
|
+
not in jobscript_deps[js_idx][js_k_idx]["js_element_mapping"][
|
|
261
|
+
js_elem_idx_i
|
|
262
|
+
]
|
|
263
|
+
):
|
|
264
|
+
jobscript_deps[js_idx][js_k_idx]["js_element_mapping"][
|
|
265
|
+
js_elem_idx_i
|
|
266
|
+
].append(js_elem_idx_k)
|
|
267
|
+
|
|
268
|
+
# next we can determine if two jobscripts have a one-to-one element mapping, which
|
|
269
|
+
# means they can be submitted with a "job array" dependency relationship:
|
|
270
|
+
for js_i_idx, deps_i in jobscript_deps.items():
|
|
271
|
+
for js_k_idx, deps_j in deps_i.items():
|
|
272
|
+
# is this an array dependency?
|
|
273
|
+
|
|
274
|
+
js_i_num_js_elements = jobscripts[js_i_idx]["EAR_ID"].shape[1]
|
|
275
|
+
js_k_num_js_elements = jobscripts[js_k_idx]["EAR_ID"].shape[1]
|
|
276
|
+
|
|
277
|
+
is_all_i_elems = sorted(set(deps_j["js_element_mapping"])) == list(
|
|
278
|
+
range(js_i_num_js_elements)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
is_all_k_single = set(
|
|
282
|
+
len(i) for i in deps_j["js_element_mapping"].values()
|
|
283
|
+
) == {1}
|
|
284
|
+
|
|
285
|
+
is_all_k_elems = sorted(
|
|
286
|
+
i[0] for i in deps_j["js_element_mapping"].values()
|
|
287
|
+
) == list(range(js_k_num_js_elements))
|
|
288
|
+
|
|
289
|
+
is_arr = is_all_i_elems and is_all_k_single and is_all_k_elems
|
|
290
|
+
jobscript_deps[js_i_idx][js_k_idx]["is_array"] = is_arr
|
|
291
|
+
|
|
292
|
+
return jobscript_deps
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _reindex_dependencies(
|
|
296
|
+
jobscripts: Mapping[int, JobScriptCreationArguments],
|
|
297
|
+
from_idx: int,
|
|
298
|
+
to_idx: int,
|
|
299
|
+
):
|
|
300
|
+
for ds_js_idx, ds_js in jobscripts.items():
|
|
301
|
+
if ds_js_idx <= from_idx:
|
|
302
|
+
continue
|
|
303
|
+
deps = ds_js["dependencies"]
|
|
304
|
+
if from_idx in deps:
|
|
305
|
+
deps[to_idx] = deps.pop(from_idx)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@TimeIt.decorator
|
|
309
|
+
def merge_jobscripts_across_tasks(
|
|
310
|
+
jobscripts: Mapping[int, JobScriptCreationArguments],
|
|
311
|
+
) -> Mapping[int, JobScriptCreationArguments]:
|
|
312
|
+
"""Try to merge jobscripts between tasks.
|
|
313
|
+
|
|
314
|
+
This is possible if two jobscripts share the same resources and have an array
|
|
315
|
+
dependency (i.e. one-to-one element dependency mapping).
|
|
316
|
+
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
# The set of IDs of dicts that we've merged, allowing us to not keep that info in
|
|
320
|
+
# the dicts themselves.
|
|
321
|
+
merged: set[int] = set()
|
|
322
|
+
|
|
323
|
+
for js_idx, js in jobscripts.items():
|
|
324
|
+
if not js["dependencies"]:
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
closest_idx = cast("int", max(js["dependencies"]))
|
|
328
|
+
closest_js = jobscripts[closest_idx]
|
|
329
|
+
other_deps = {k: v for k, v in js["dependencies"].items() if k != closest_idx}
|
|
330
|
+
|
|
331
|
+
# if all `other_deps` are also found within `closest_js`'s dependencies, then we
|
|
332
|
+
# can merge `js` into `closest_js`:
|
|
333
|
+
merge = True
|
|
334
|
+
for dep_idx, dep_i in other_deps.items():
|
|
335
|
+
try:
|
|
336
|
+
if closest_js["dependencies"][dep_idx] != dep_i:
|
|
337
|
+
merge = False
|
|
338
|
+
except KeyError:
|
|
339
|
+
merge = False
|
|
340
|
+
|
|
341
|
+
if merge:
|
|
342
|
+
js_j = closest_js # the jobscript we are merging `js` into
|
|
343
|
+
js_j_idx = closest_idx
|
|
344
|
+
dep_info = js["dependencies"][js_j_idx]
|
|
345
|
+
|
|
346
|
+
# can only merge if resources are the same and is array dependency:
|
|
347
|
+
if js["resource_hash"] == js_j["resource_hash"] and dep_info["is_array"]:
|
|
348
|
+
num_loop_idx = len(
|
|
349
|
+
js_j["task_loop_idx"]
|
|
350
|
+
) # TODO: should this be: `js_j["task_loop_idx"][0]`?
|
|
351
|
+
|
|
352
|
+
# append task_insert_IDs
|
|
353
|
+
js_j["task_insert_IDs"].append(js["task_insert_IDs"][0])
|
|
354
|
+
js_j["task_loop_idx"].append(js["task_loop_idx"][0])
|
|
355
|
+
|
|
356
|
+
add_acts = [(a, b, num_loop_idx) for a, b, _ in js["task_actions"]]
|
|
357
|
+
|
|
358
|
+
js_j["task_actions"].extend(add_acts)
|
|
359
|
+
for k, v in js["task_elements"].items():
|
|
360
|
+
js_j["task_elements"][k].extend(v)
|
|
361
|
+
|
|
362
|
+
# append to elements and elements_idx list
|
|
363
|
+
js_j["EAR_ID"] = np.vstack((js_j["EAR_ID"], js["EAR_ID"]))
|
|
364
|
+
|
|
365
|
+
# mark this js as defunct
|
|
366
|
+
merged.add(id(js))
|
|
367
|
+
|
|
368
|
+
# update dependencies of any downstream jobscripts that refer to this js
|
|
369
|
+
_reindex_dependencies(jobscripts, js_idx, js_j_idx)
|
|
370
|
+
|
|
371
|
+
# remove is_merged jobscripts:
|
|
372
|
+
return {k: v for k, v in jobscripts.items() if id(v) not in merged}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
@TimeIt.decorator
|
|
376
|
+
def resolve_jobscript_blocks(
|
|
377
|
+
jobscripts: Mapping[int, JobScriptCreationArguments],
|
|
378
|
+
) -> list[dict[str, Any]]:
|
|
379
|
+
"""For contiguous, dependent, non-array jobscripts with identical resource
|
|
380
|
+
requirements, combine into multi-block jobscripts.
|
|
381
|
+
|
|
382
|
+
Parameters
|
|
383
|
+
----------
|
|
384
|
+
jobscripts
|
|
385
|
+
Dict whose values must be dicts with keys "is_array", "resource_hash" and
|
|
386
|
+
"dependencies".
|
|
387
|
+
run_parallelism
|
|
388
|
+
True if the store supports run parallelism
|
|
389
|
+
|
|
390
|
+
"""
|
|
391
|
+
js_new: list[list[JobScriptCreationArguments]] = (
|
|
392
|
+
[]
|
|
393
|
+
) # TODO: not the same type, e.g. dependencies have tuple keys,
|
|
394
|
+
new_idx: dict[int, tuple[int, int]] = (
|
|
395
|
+
{}
|
|
396
|
+
) # track new positions by new jobscript index and block index
|
|
397
|
+
new_idx_inv: dict[int, list[int]] = defaultdict(list)
|
|
398
|
+
prev_hash = None
|
|
399
|
+
blocks: list[JobScriptCreationArguments] = []
|
|
400
|
+
js_deps_rec: dict[int, set[int]] = {} # recursive
|
|
401
|
+
for js_idx, js_i in jobscripts.items():
|
|
402
|
+
|
|
403
|
+
cur_js_idx = len(js_new)
|
|
404
|
+
new_deps_js_j = {
|
|
405
|
+
new_idx[i][0] for i in cast("Sequence[int]", js_i["dependencies"])
|
|
406
|
+
}
|
|
407
|
+
new_deps_js_j_rec = [
|
|
408
|
+
k for i in new_deps_js_j for j in new_idx_inv[i] for k in js_deps_rec[j]
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
js_deps_rec[js_idx] = new_deps_js_j.union(new_deps_js_j_rec)
|
|
412
|
+
|
|
413
|
+
# recursive dependencies of js_i (which we're looking to merge), excluding the
|
|
414
|
+
# dependency on the current jobscript:
|
|
415
|
+
js_j_deps_rec_no_cur = js_deps_rec[js_idx] - set([cur_js_idx])
|
|
416
|
+
|
|
417
|
+
# recursive dependencies of the current jobscript:
|
|
418
|
+
cur_deps_rec = {
|
|
419
|
+
j for i in new_idx_inv[cur_js_idx] for j in js_deps_rec[i] if j != cur_js_idx
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# can we mege js_i into the current jobscript, as far as dependencies are
|
|
423
|
+
# concerned?
|
|
424
|
+
deps_mergable = cur_js_idx in new_deps_js_j
|
|
425
|
+
if deps_mergable and js_j_deps_rec_no_cur:
|
|
426
|
+
deps_mergable = js_j_deps_rec_no_cur == cur_deps_rec
|
|
427
|
+
|
|
428
|
+
if js_i["is_array"]:
|
|
429
|
+
# array jobs cannot be merged into the same jobscript
|
|
430
|
+
|
|
431
|
+
# append existing block:
|
|
432
|
+
if blocks:
|
|
433
|
+
js_new.append(blocks)
|
|
434
|
+
prev_hash = None
|
|
435
|
+
blocks = []
|
|
436
|
+
|
|
437
|
+
new_idx[js_idx] = (len(js_new), 0)
|
|
438
|
+
new_idx_inv[len(js_new)].append(js_idx)
|
|
439
|
+
js_new.append([js_i])
|
|
440
|
+
continue
|
|
441
|
+
|
|
442
|
+
if js_idx == 0 or prev_hash is None:
|
|
443
|
+
# (note: zeroth index will always exist)
|
|
444
|
+
|
|
445
|
+
# start a new block:
|
|
446
|
+
blocks.append(js_i)
|
|
447
|
+
new_idx[js_idx] = (len(js_new), len(blocks) - 1)
|
|
448
|
+
new_idx_inv[len(js_new)].append(js_idx)
|
|
449
|
+
|
|
450
|
+
# set resource hash to compare with the next jobscript
|
|
451
|
+
prev_hash = js_i["resource_hash"]
|
|
452
|
+
|
|
453
|
+
elif js_i["resource_hash"] == prev_hash and deps_mergable:
|
|
454
|
+
# merge with previous jobscript by adding another block
|
|
455
|
+
# only merge if this jobscript's dependencies include the current jobscript,
|
|
456
|
+
# and any other dependencies are included in the current jobscript's
|
|
457
|
+
# dependencies
|
|
458
|
+
blocks.append(js_i)
|
|
459
|
+
new_idx[js_idx] = (len(js_new), len(blocks) - 1)
|
|
460
|
+
new_idx_inv[len(js_new)].append(js_idx)
|
|
461
|
+
|
|
462
|
+
else:
|
|
463
|
+
# cannot merge, append the new jobscript data:
|
|
464
|
+
js_new.append(blocks)
|
|
465
|
+
|
|
466
|
+
# start a new block:
|
|
467
|
+
blocks = [js_i]
|
|
468
|
+
new_idx[js_idx] = (len(js_new), len(blocks) - 1)
|
|
469
|
+
new_idx_inv[len(js_new)].append(js_idx)
|
|
470
|
+
|
|
471
|
+
# set resource hash to compare with the next jobscript
|
|
472
|
+
prev_hash = js_i["resource_hash"]
|
|
473
|
+
|
|
474
|
+
# append remaining blocks:
|
|
475
|
+
if blocks:
|
|
476
|
+
js_new.append(blocks)
|
|
477
|
+
prev_hash = None
|
|
478
|
+
blocks = []
|
|
479
|
+
|
|
480
|
+
# re-index dependencies:
|
|
481
|
+
js_new_: list[dict[str, Any]] = []
|
|
482
|
+
for js_i_idx, js_new_i in enumerate(js_new):
|
|
483
|
+
|
|
484
|
+
resources = None
|
|
485
|
+
is_array = None
|
|
486
|
+
for block_j in js_new_i:
|
|
487
|
+
for k, v in new_idx.items():
|
|
488
|
+
dep_data = block_j["dependencies"].pop(k, None)
|
|
489
|
+
if dep_data:
|
|
490
|
+
block_j["dependencies"][v] = dep_data
|
|
491
|
+
|
|
492
|
+
del block_j["resource_hash"]
|
|
493
|
+
resources = block_j.pop("resources", None)
|
|
494
|
+
is_array = block_j.pop("is_array")
|
|
495
|
+
|
|
496
|
+
js_new_.append(
|
|
497
|
+
{
|
|
498
|
+
"resources": resources,
|
|
499
|
+
"is_array": is_array,
|
|
500
|
+
"blocks": js_new[js_i_idx],
|
|
501
|
+
}
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
return js_new_
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
@hydrate
|
|
508
|
+
class JobscriptBlock(JSONLike):
|
|
509
|
+
"""A rectangular block of element-actions to run within a jobscript.
|
|
510
|
+
|
|
511
|
+
Parameters
|
|
512
|
+
----------
|
|
513
|
+
task_insert_IDs: list[int]
|
|
514
|
+
The task insertion IDs.
|
|
515
|
+
task_actions: list[tuple]
|
|
516
|
+
The actions of the tasks.
|
|
517
|
+
``task insert ID, action_idx, index into task_loop_idx`` for each ``JS_ACTION_IDX``
|
|
518
|
+
task_elements: dict[int, list[int]]
|
|
519
|
+
The elements of the tasks.
|
|
520
|
+
Maps ``JS_ELEMENT_IDX`` to list of ``TASK_ELEMENT_IDX`` for each ``TASK_INSERT_ID``
|
|
521
|
+
EAR_ID:
|
|
522
|
+
Element action run information.
|
|
523
|
+
task_loop_idx: list[dict]
|
|
524
|
+
Description of what loops are in play.
|
|
525
|
+
dependencies: dict[tuple[int, int], dict]
|
|
526
|
+
Description of dependencies. Keys are tuples of (jobscript index,
|
|
527
|
+
jobscript-block index) of the dependency.
|
|
528
|
+
index: int
|
|
529
|
+
The index of the block within the parent jobscript.
|
|
530
|
+
jobscript: ~hpcflow.app.Jobscript
|
|
531
|
+
The parent jobscript.
|
|
532
|
+
|
|
533
|
+
"""
|
|
534
|
+
|
|
535
|
+
def __init__(
|
|
536
|
+
self,
|
|
537
|
+
index: int,
|
|
538
|
+
task_insert_IDs: list[int],
|
|
539
|
+
task_loop_idx: list[dict[str, int]],
|
|
540
|
+
task_actions: list[tuple[int, int, int]] | None = None,
|
|
541
|
+
task_elements: dict[int, list[int]] | None = None,
|
|
542
|
+
EAR_ID: NDArray | None = None,
|
|
543
|
+
dependencies: (
|
|
544
|
+
dict[tuple[int, int], ResolvedJobscriptBlockDependencies] | None
|
|
545
|
+
) = None,
|
|
546
|
+
jobscript: Jobscript | None = None,
|
|
547
|
+
):
|
|
548
|
+
self.jobscript = jobscript
|
|
549
|
+
self._index = index
|
|
550
|
+
self._task_insert_IDs = task_insert_IDs
|
|
551
|
+
self._task_actions = task_actions
|
|
552
|
+
self._task_elements = task_elements
|
|
553
|
+
self._task_loop_idx = task_loop_idx
|
|
554
|
+
self._EAR_ID = EAR_ID
|
|
555
|
+
self._dependencies = dependencies
|
|
556
|
+
|
|
557
|
+
self._all_EARs = None # assigned on first access to `all_EARs` property
|
|
558
|
+
|
|
559
|
+
@property
|
|
560
|
+
def index(self) -> int:
|
|
561
|
+
return self._index
|
|
562
|
+
|
|
563
|
+
@property
|
|
564
|
+
def submission(self) -> Submission:
|
|
565
|
+
assert self.jobscript is not None
|
|
566
|
+
return self.jobscript.submission
|
|
567
|
+
|
|
568
|
+
@property
|
|
569
|
+
def task_insert_IDs(self) -> Sequence[int]:
|
|
570
|
+
"""
|
|
571
|
+
The insertion IDs of tasks in this jobscript-block.
|
|
572
|
+
"""
|
|
573
|
+
return self._task_insert_IDs
|
|
574
|
+
|
|
575
|
+
@property
|
|
576
|
+
@TimeIt.decorator
|
|
577
|
+
def task_actions(self) -> NDArray:
|
|
578
|
+
"""
|
|
579
|
+
The IDs of actions of each task in this jobscript-block.
|
|
580
|
+
"""
|
|
581
|
+
assert self.jobscript is not None
|
|
582
|
+
return self.workflow._store.get_jobscript_block_task_actions_array(
|
|
583
|
+
sub_idx=self.submission.index,
|
|
584
|
+
js_idx=self.jobscript.index,
|
|
585
|
+
blk_idx=self.index,
|
|
586
|
+
task_actions_arr=self._task_actions,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
@property
|
|
590
|
+
@TimeIt.decorator
|
|
591
|
+
def task_elements(self) -> Mapping[int, Sequence[int]]:
|
|
592
|
+
"""
|
|
593
|
+
The IDs of elements of each task in this jobscript-block.
|
|
594
|
+
"""
|
|
595
|
+
assert self.jobscript is not None
|
|
596
|
+
return self.workflow._store.get_jobscript_block_task_elements_map(
|
|
597
|
+
sub_idx=self.submission.index,
|
|
598
|
+
js_idx=self.jobscript.index,
|
|
599
|
+
blk_idx=self.index,
|
|
600
|
+
task_elems_map=self._task_elements,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
@property
|
|
604
|
+
@TimeIt.decorator
|
|
605
|
+
def EAR_ID(self) -> NDArray:
|
|
606
|
+
"""
|
|
607
|
+
The array of EAR IDs in this jobscript-block.
|
|
608
|
+
"""
|
|
609
|
+
assert self.jobscript is not None
|
|
610
|
+
return self.workflow._store.get_jobscript_block_run_ID_array(
|
|
611
|
+
sub_idx=self.submission.index,
|
|
612
|
+
js_idx=self.jobscript.index,
|
|
613
|
+
blk_idx=self.index,
|
|
614
|
+
run_ID_arr=self._EAR_ID,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
@property
|
|
618
|
+
@TimeIt.decorator
|
|
619
|
+
def dependencies(
|
|
620
|
+
self,
|
|
621
|
+
) -> Mapping[tuple[int, int], ResolvedJobscriptBlockDependencies]:
|
|
622
|
+
"""
|
|
623
|
+
The dependency descriptor.
|
|
624
|
+
"""
|
|
625
|
+
assert self.jobscript is not None
|
|
626
|
+
return self.workflow._store.get_jobscript_block_dependencies(
|
|
627
|
+
sub_idx=self.submission.index,
|
|
628
|
+
js_idx=self.jobscript.index,
|
|
629
|
+
blk_idx=self.index,
|
|
630
|
+
js_dependencies=self._dependencies,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
@property
|
|
634
|
+
def task_loop_idx(self) -> Sequence[Mapping[str, int]]:
|
|
635
|
+
"""
|
|
636
|
+
The description of where various task loops are.
|
|
637
|
+
"""
|
|
638
|
+
return self._task_loop_idx
|
|
639
|
+
|
|
640
|
+
@property
|
|
641
|
+
@TimeIt.decorator
|
|
642
|
+
def num_actions(self) -> int:
|
|
643
|
+
"""
|
|
644
|
+
The maximal number of actions in the jobscript-block.
|
|
645
|
+
"""
|
|
646
|
+
return self.EAR_ID.shape[0]
|
|
647
|
+
|
|
648
|
+
@property
|
|
649
|
+
@TimeIt.decorator
|
|
650
|
+
def num_elements(self) -> int:
|
|
651
|
+
"""
|
|
652
|
+
The maximal number of elements in the jobscript-block.
|
|
653
|
+
"""
|
|
654
|
+
return self.EAR_ID.shape[1]
|
|
655
|
+
|
|
656
|
+
@property
|
|
657
|
+
def workflow(self) -> Workflow:
|
|
658
|
+
"""
|
|
659
|
+
The associated workflow.
|
|
660
|
+
"""
|
|
661
|
+
assert self.jobscript is not None
|
|
662
|
+
return self.jobscript.workflow
|
|
663
|
+
|
|
664
|
+
@property
|
|
665
|
+
@TimeIt.decorator
|
|
666
|
+
def all_EARs(self) -> Sequence[ElementActionRun]:
|
|
667
|
+
"""
|
|
668
|
+
Description of EAR information for this jobscript-block.
|
|
669
|
+
"""
|
|
670
|
+
assert self.jobscript is not None
|
|
671
|
+
return [i for i in self.jobscript.all_EARs if i.id_ in self.EAR_ID]
|
|
672
|
+
|
|
673
|
+
@override
|
|
674
|
+
def _postprocess_to_dict(self, d: dict[str, Any]) -> dict[str, Any]:
|
|
675
|
+
dct = super()._postprocess_to_dict(d)
|
|
676
|
+
del dct["_all_EARs"]
|
|
677
|
+
dct["_dependencies"] = [[list(k), v] for k, v in self.dependencies.items()]
|
|
678
|
+
dct = {k.lstrip("_"): v for k, v in dct.items()}
|
|
679
|
+
dct["EAR_ID"] = cast("NDArray", dct["EAR_ID"]).tolist()
|
|
680
|
+
return dct
|
|
681
|
+
|
|
682
|
+
@classmethod
|
|
683
|
+
def from_json_like(cls, json_like, shared_data=None):
|
|
684
|
+
json_like["EAR_ID"] = (
|
|
685
|
+
np.array(json_like["EAR_ID"]) if json_like["EAR_ID"] is not None else None
|
|
686
|
+
)
|
|
687
|
+
if json_like["dependencies"] is not None:
|
|
688
|
+
# transform list to dict with tuple keys, and transform string keys in
|
|
689
|
+
# `js_element_mapping` to integers:
|
|
690
|
+
deps_processed = {}
|
|
691
|
+
for i in json_like["dependencies"]:
|
|
692
|
+
deps_processed_i = {
|
|
693
|
+
"js_element_mapping": {
|
|
694
|
+
int(k): v for k, v in i[1]["js_element_mapping"].items()
|
|
695
|
+
},
|
|
696
|
+
"is_array": i[1]["is_array"],
|
|
697
|
+
}
|
|
698
|
+
deps_processed[tuple(i[0])] = deps_processed_i
|
|
699
|
+
json_like["dependencies"] = deps_processed
|
|
700
|
+
|
|
701
|
+
return super().from_json_like(json_like, shared_data)
|
|
702
|
+
|
|
703
|
+
def _get_EARs_arr(self) -> NDArray:
|
|
704
|
+
"""
|
|
705
|
+
Get all associated EAR objects as a 2D array.
|
|
706
|
+
"""
|
|
707
|
+
return np.array(self.all_EARs).reshape(self.EAR_ID.shape)
|
|
708
|
+
|
|
709
|
+
def get_task_loop_idx_array(self) -> NDArray:
|
|
710
|
+
"""
|
|
711
|
+
Get an array of task loop indices.
|
|
712
|
+
"""
|
|
713
|
+
loop_idx = np.empty_like(self.EAR_ID)
|
|
714
|
+
loop_idx[:] = np.array([i[2] for i in self.task_actions]).reshape(
|
|
715
|
+
(len(self.task_actions), 1)
|
|
716
|
+
)
|
|
717
|
+
return loop_idx
|
|
718
|
+
|
|
719
|
+
@TimeIt.decorator
|
|
720
|
+
def write_EAR_ID_file(self, fp: TextIO):
|
|
721
|
+
"""Write a text file with `num_elements` lines and `num_actions` delimited tokens
|
|
722
|
+
per line, representing whether a given EAR must be executed."""
|
|
723
|
+
assert self.jobscript is not None
|
|
724
|
+
# can't specify "open" newline if we pass the file name only, so pass handle:
|
|
725
|
+
np.savetxt(
|
|
726
|
+
fname=fp,
|
|
727
|
+
X=(self.EAR_ID).T,
|
|
728
|
+
fmt="%.0f",
|
|
729
|
+
delimiter=self.jobscript._EAR_files_delimiter,
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
@hydrate
|
|
734
|
+
class Jobscript(JSONLike):
|
|
735
|
+
"""
|
|
736
|
+
A group of actions that are submitted together to be executed by the underlying job
|
|
737
|
+
management system as a single unit.
|
|
738
|
+
|
|
739
|
+
Parameters
|
|
740
|
+
----------
|
|
741
|
+
task_insert_IDs: list[int]
|
|
742
|
+
The task insertion IDs.
|
|
743
|
+
task_actions: list[tuple]
|
|
744
|
+
The actions of the tasks.
|
|
745
|
+
``task insert ID, action_idx, index into task_loop_idx`` for each ``JS_ACTION_IDX``
|
|
746
|
+
task_elements: dict[int, list[int]]
|
|
747
|
+
The elements of the tasks.
|
|
748
|
+
Maps ``JS_ELEMENT_IDX`` to list of ``TASK_ELEMENT_IDX`` for each ``TASK_INSERT_ID``
|
|
749
|
+
EAR_ID:
|
|
750
|
+
Element action run information.
|
|
751
|
+
resources: ~hpcflow.app.ElementResources
|
|
752
|
+
Resources to use
|
|
753
|
+
task_loop_idx: list[dict]
|
|
754
|
+
Description of what loops are in play.
|
|
755
|
+
dependencies: dict[int, dict]
|
|
756
|
+
Description of dependencies.
|
|
757
|
+
submit_time: datetime
|
|
758
|
+
When the jobscript was submitted, if known.
|
|
759
|
+
submit_hostname: str
|
|
760
|
+
Where the jobscript was submitted, if known.
|
|
761
|
+
submit_machine: str
|
|
762
|
+
Description of what the jobscript was submitted to, if known.
|
|
763
|
+
submit_cmdline: str
|
|
764
|
+
The command line used to do the commit, if known.
|
|
765
|
+
scheduler_job_ID: str
|
|
766
|
+
The job ID from the scheduler, if known.
|
|
767
|
+
process_ID: int
|
|
768
|
+
The process ID of the subprocess, if known.
|
|
769
|
+
version_info: dict[str, ...]
|
|
770
|
+
Version info about the target system.
|
|
771
|
+
os_name: str
|
|
772
|
+
The name of the OS.
|
|
773
|
+
shell_name: str
|
|
774
|
+
The name of the shell.
|
|
775
|
+
scheduler_name: str
|
|
776
|
+
The scheduler used.
|
|
777
|
+
running: bool
|
|
778
|
+
Whether the jobscript is currently running.
|
|
779
|
+
"""
|
|
780
|
+
|
|
781
|
+
_EAR_files_delimiter: ClassVar[str] = ":"
|
|
782
|
+
_workflow_app_alias: ClassVar[str] = "wkflow_app"
|
|
783
|
+
|
|
784
|
+
_child_objects: ClassVar[tuple[ChildObjectSpec, ...]] = (
|
|
785
|
+
ChildObjectSpec(
|
|
786
|
+
name="resources",
|
|
787
|
+
class_name="ElementResources",
|
|
788
|
+
),
|
|
789
|
+
ChildObjectSpec(
|
|
790
|
+
name="blocks",
|
|
791
|
+
class_name="JobscriptBlock",
|
|
792
|
+
is_multiple=True,
|
|
793
|
+
parent_ref="jobscript",
|
|
794
|
+
),
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
def __init__(
|
|
798
|
+
self,
|
|
799
|
+
index: int,
|
|
800
|
+
is_array: bool,
|
|
801
|
+
resources: ElementResources,
|
|
802
|
+
blocks: list[JobscriptBlock],
|
|
803
|
+
at_submit_metadata: dict[str, Any] | None = None,
|
|
804
|
+
submit_hostname: str | None = None,
|
|
805
|
+
submit_machine: str | None = None,
|
|
806
|
+
shell_idx: int | None = None,
|
|
807
|
+
version_info: VersionInfo | None = None,
|
|
808
|
+
resource_hash: str | None = None,
|
|
809
|
+
elements: dict[int, list[int]] | None = None,
|
|
810
|
+
):
|
|
811
|
+
if resource_hash is not None:
|
|
812
|
+
raise AttributeError("resource_hash must not be supplied")
|
|
813
|
+
if elements is not None:
|
|
814
|
+
raise AttributeError("elements must not be supplied")
|
|
815
|
+
|
|
816
|
+
if not isinstance(blocks[0], JobscriptBlock):
|
|
817
|
+
blocks = [
|
|
818
|
+
JobscriptBlock(**i, index=idx, jobscript=self)
|
|
819
|
+
for idx, i in enumerate(blocks)
|
|
820
|
+
]
|
|
821
|
+
|
|
822
|
+
self._index = index
|
|
823
|
+
self._blocks = blocks
|
|
824
|
+
self._at_submit_metadata = at_submit_metadata or {
|
|
825
|
+
k: None for k in JOBSCRIPT_SUBMIT_TIME_KEYS
|
|
826
|
+
}
|
|
827
|
+
self._is_array = is_array
|
|
828
|
+
self._resources = resources
|
|
829
|
+
|
|
830
|
+
# assigned on parent `Submission.submit` (or retrieved form persistent store):
|
|
831
|
+
self._submit_hostname = submit_hostname
|
|
832
|
+
self._submit_machine = submit_machine
|
|
833
|
+
self._shell_idx = shell_idx
|
|
834
|
+
|
|
835
|
+
self._version_info = version_info
|
|
836
|
+
|
|
837
|
+
# assigned by parent Submission
|
|
838
|
+
self._submission: Submission | None = None
|
|
839
|
+
# assigned on first access to `scheduler` property
|
|
840
|
+
self._scheduler_obj: Scheduler | None = None
|
|
841
|
+
# assigned on first access to `shell` property
|
|
842
|
+
self._shell_obj: Shell | None = None
|
|
843
|
+
# assigned on first access to `submit_time` property
|
|
844
|
+
self._submit_time_obj: datetime | None = None
|
|
845
|
+
# assigned on first access to `all_EARs` property
|
|
846
|
+
self._all_EARs: list[ElementActionRun] | None = None
|
|
847
|
+
|
|
848
|
+
self._set_parent_refs()
|
|
849
|
+
|
|
850
|
+
def __repr__(self) -> str:
|
|
851
|
+
return (
|
|
852
|
+
f"{self.__class__.__name__}("
|
|
853
|
+
f"index={self.index!r}, "
|
|
854
|
+
f"blocks={self.blocks!r}, "
|
|
855
|
+
f"resources={self.resources!r}, "
|
|
856
|
+
f")"
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
@override
|
|
860
|
+
def _postprocess_to_dict(self, d: dict[str, Any]) -> dict[str, Any]:
|
|
861
|
+
dct = super()._postprocess_to_dict(d)
|
|
862
|
+
del dct["_scheduler_obj"]
|
|
863
|
+
del dct["_shell_obj"]
|
|
864
|
+
del dct["_submit_time_obj"]
|
|
865
|
+
del dct["_all_EARs"]
|
|
866
|
+
dct = {k.lstrip("_"): v for k, v in dct.items()}
|
|
867
|
+
return dct
|
|
868
|
+
|
|
869
|
+
@classmethod
|
|
870
|
+
def from_json_like(cls, json_like, shared_data=None):
|
|
871
|
+
return super().from_json_like(json_like, shared_data)
|
|
872
|
+
|
|
873
|
+
@property
|
|
874
|
+
def workflow_app_alias(self) -> str:
|
|
875
|
+
"""
|
|
876
|
+
Alias for the workflow app in job scripts.
|
|
877
|
+
"""
|
|
878
|
+
return self.submission.WORKFLOW_APP_ALIAS
|
|
879
|
+
|
|
880
|
+
def get_commands_file_name(
|
|
881
|
+
self, block_act_key: BlockActionKey, shell: Shell | None = None
|
|
882
|
+
) -> str:
|
|
883
|
+
"""
|
|
884
|
+
Get the name of a file containing commands for a particular jobscript action.
|
|
885
|
+
"""
|
|
886
|
+
return self._app.RunDirAppFiles.get_commands_file_name(
|
|
887
|
+
block_act_key,
|
|
888
|
+
shell=shell or self.shell,
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
@property
|
|
892
|
+
def blocks(self) -> Sequence[JobscriptBlock]:
|
|
893
|
+
return self._blocks
|
|
894
|
+
|
|
895
|
+
@property
|
|
896
|
+
def at_submit_metadata(self) -> dict[str, Any]:
|
|
897
|
+
return self.workflow._store.get_jobscript_at_submit_metadata(
|
|
898
|
+
sub_idx=self.submission.index,
|
|
899
|
+
js_idx=self.index,
|
|
900
|
+
metadata_attr=self._at_submit_metadata,
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
@property
|
|
904
|
+
@TimeIt.decorator
|
|
905
|
+
def all_EAR_IDs(self) -> NDArray:
|
|
906
|
+
"""Return all run IDs of this jobscripts (across all blocks), removing missing
|
|
907
|
+
run IDs (i.e. -1 values)"""
|
|
908
|
+
return np.concatenate([i.EAR_ID[i.EAR_ID >= 0] for i in self.blocks])
|
|
909
|
+
|
|
910
|
+
@property
|
|
911
|
+
@TimeIt.decorator
|
|
912
|
+
def all_EARs(self) -> Sequence[ElementActionRun]:
|
|
913
|
+
"""
|
|
914
|
+
Description of EAR information for this jobscript.
|
|
915
|
+
"""
|
|
916
|
+
if self.submission._use_EARs_cache:
|
|
917
|
+
return [self.submission._EARs_cache[ear_id] for ear_id in self.all_EAR_IDs]
|
|
918
|
+
return self.workflow.get_EARs_from_IDs(self.all_EAR_IDs)
|
|
919
|
+
|
|
920
|
+
@property
|
|
921
|
+
@TimeIt.decorator
|
|
922
|
+
def resources(self) -> ElementResources:
|
|
923
|
+
"""
|
|
924
|
+
The common resources that this jobscript requires.
|
|
925
|
+
"""
|
|
926
|
+
return self._resources
|
|
927
|
+
|
|
928
|
+
@property
|
|
929
|
+
@TimeIt.decorator
|
|
930
|
+
def dependencies(self) -> Mapping[tuple[int, int], dict[str, bool]]:
|
|
931
|
+
"""
|
|
932
|
+
The dependency descriptor, accounting for all blocks within this jobscript.
|
|
933
|
+
"""
|
|
934
|
+
deps = {}
|
|
935
|
+
for block in self.blocks:
|
|
936
|
+
for (js_idx, blk_idx), v in block.dependencies.items():
|
|
937
|
+
if js_idx == self.index:
|
|
938
|
+
# block dependency is internal to this jobscript
|
|
939
|
+
continue
|
|
940
|
+
else:
|
|
941
|
+
deps[js_idx, blk_idx] = {"is_array": v["is_array"]}
|
|
942
|
+
return deps
|
|
943
|
+
|
|
944
|
+
@property
|
|
945
|
+
@TimeIt.decorator
|
|
946
|
+
def start_time(self) -> None | datetime:
|
|
947
|
+
"""The first known start time of any EAR in this jobscript."""
|
|
948
|
+
if not self.is_submitted:
|
|
949
|
+
return None
|
|
950
|
+
return min(
|
|
951
|
+
(ear.start_time for ear in self.all_EARs if ear.start_time), default=None
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
@property
|
|
955
|
+
@TimeIt.decorator
|
|
956
|
+
def end_time(self) -> None | datetime:
|
|
957
|
+
"""The last known end time of any EAR in this jobscript."""
|
|
958
|
+
if not self.is_submitted:
|
|
959
|
+
return None
|
|
960
|
+
return max((ear.end_time for ear in self.all_EARs if ear.end_time), default=None)
|
|
961
|
+
|
|
962
|
+
@property
|
|
963
|
+
def submit_time(self):
|
|
964
|
+
"""
|
|
965
|
+
When the jobscript was submitted, if known.
|
|
966
|
+
"""
|
|
967
|
+
if self._submit_time_obj is None:
|
|
968
|
+
if _submit_time := self.at_submit_metadata["submit_time"]:
|
|
969
|
+
self._submit_time_obj = parse_timestamp(
|
|
970
|
+
_submit_time, self.workflow.ts_fmt
|
|
971
|
+
)
|
|
972
|
+
return self._submit_time_obj
|
|
973
|
+
|
|
974
|
+
@property
|
|
975
|
+
def submit_hostname(self) -> str | None:
|
|
976
|
+
"""
|
|
977
|
+
Where the jobscript was submitted, if known.
|
|
978
|
+
"""
|
|
979
|
+
return self._submit_hostname
|
|
980
|
+
|
|
981
|
+
@property
|
|
982
|
+
def submit_machine(self) -> str | None:
|
|
983
|
+
"""
|
|
984
|
+
Description of what the jobscript was submitted to, if known.
|
|
985
|
+
"""
|
|
986
|
+
return self._submit_machine
|
|
987
|
+
|
|
988
|
+
@property
|
|
989
|
+
def shell_idx(self):
|
|
990
|
+
return self._shell_idx
|
|
991
|
+
|
|
992
|
+
@property
|
|
993
|
+
def submit_cmdline(self) -> list[str] | None:
|
|
994
|
+
"""
|
|
995
|
+
The command line used to submit the jobscript, if known.
|
|
996
|
+
"""
|
|
997
|
+
return self.at_submit_metadata["submit_cmdline"]
|
|
998
|
+
|
|
999
|
+
@property
|
|
1000
|
+
def scheduler_job_ID(self) -> str | None:
|
|
1001
|
+
"""
|
|
1002
|
+
The job ID from the scheduler, if known.
|
|
1003
|
+
"""
|
|
1004
|
+
return self.at_submit_metadata["scheduler_job_ID"]
|
|
1005
|
+
|
|
1006
|
+
@property
|
|
1007
|
+
def process_ID(self) -> int | None:
|
|
1008
|
+
"""
|
|
1009
|
+
The process ID from direct execution, if known.
|
|
1010
|
+
"""
|
|
1011
|
+
return self.at_submit_metadata["process_ID"]
|
|
1012
|
+
|
|
1013
|
+
@property
|
|
1014
|
+
def version_info(self) -> VersionInfo | None:
|
|
1015
|
+
"""
|
|
1016
|
+
Version information about the execution environment (OS, etc).
|
|
1017
|
+
"""
|
|
1018
|
+
return self._version_info
|
|
1019
|
+
|
|
1020
|
+
@property
|
|
1021
|
+
def index(self) -> int:
|
|
1022
|
+
"""
|
|
1023
|
+
The index of this jobscript within its parent :py:class:`Submission`.
|
|
1024
|
+
"""
|
|
1025
|
+
assert self._index is not None
|
|
1026
|
+
return self._index
|
|
1027
|
+
|
|
1028
|
+
@property
|
|
1029
|
+
def submission(self) -> Submission:
|
|
1030
|
+
"""
|
|
1031
|
+
The parent submission.
|
|
1032
|
+
"""
|
|
1033
|
+
assert self._submission is not None
|
|
1034
|
+
return self._submission
|
|
1035
|
+
|
|
1036
|
+
@property
|
|
1037
|
+
def workflow(self) -> Workflow:
|
|
1038
|
+
"""
|
|
1039
|
+
The workflow this is all on behalf of.
|
|
1040
|
+
"""
|
|
1041
|
+
return self.submission.workflow
|
|
1042
|
+
|
|
1043
|
+
@property
|
|
1044
|
+
def is_array(self) -> bool:
|
|
1045
|
+
"""
|
|
1046
|
+
Whether to generate an array job.
|
|
1047
|
+
"""
|
|
1048
|
+
return self._is_array
|
|
1049
|
+
|
|
1050
|
+
@property
|
|
1051
|
+
def os_name(self) -> str:
|
|
1052
|
+
"""
|
|
1053
|
+
The name of the OS to use.
|
|
1054
|
+
"""
|
|
1055
|
+
assert self.resources.os_name
|
|
1056
|
+
return self.resources.os_name
|
|
1057
|
+
|
|
1058
|
+
@property
|
|
1059
|
+
def shell_name(self) -> str:
|
|
1060
|
+
assert self.resources.shell
|
|
1061
|
+
return self.resources.shell
|
|
1062
|
+
|
|
1063
|
+
@property
|
|
1064
|
+
def scheduler_name(self) -> str:
|
|
1065
|
+
"""
|
|
1066
|
+
The name of the scheduler to use.
|
|
1067
|
+
"""
|
|
1068
|
+
assert self.resources.scheduler
|
|
1069
|
+
return self.resources.scheduler
|
|
1070
|
+
|
|
1071
|
+
def _get_submission_os_args(self) -> dict[str, str]:
|
|
1072
|
+
return {"linux_release_file": self._app.config.linux_release_file}
|
|
1073
|
+
|
|
1074
|
+
def _get_submission_shell_args(self) -> dict[str, Any]:
|
|
1075
|
+
return self.resources.shell_args
|
|
1076
|
+
|
|
1077
|
+
def _get_submission_scheduler_args(self) -> dict[str, Any]:
|
|
1078
|
+
return self.resources.scheduler_args
|
|
1079
|
+
|
|
1080
|
+
def _get_shell(
|
|
1081
|
+
self,
|
|
1082
|
+
os_name: str,
|
|
1083
|
+
shell_name: str | None,
|
|
1084
|
+
os_args: dict[str, Any] | None = None,
|
|
1085
|
+
shell_args: dict[str, Any] | None = None,
|
|
1086
|
+
) -> Shell:
|
|
1087
|
+
"""Get an arbitrary shell, not necessarily associated with submission."""
|
|
1088
|
+
return get_shell(
|
|
1089
|
+
shell_name=shell_name,
|
|
1090
|
+
os_name=os_name,
|
|
1091
|
+
os_args=os_args or {},
|
|
1092
|
+
**(shell_args or {}),
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
@property
|
|
1096
|
+
def shell(self) -> Shell:
|
|
1097
|
+
"""The shell for composing submission scripts."""
|
|
1098
|
+
if self._shell_obj is None:
|
|
1099
|
+
self._shell_obj = self._get_shell(
|
|
1100
|
+
os_name=self.os_name,
|
|
1101
|
+
shell_name=self.shell_name,
|
|
1102
|
+
os_args=self._get_submission_os_args(),
|
|
1103
|
+
shell_args=self._get_submission_shell_args(),
|
|
1104
|
+
)
|
|
1105
|
+
return self._shell_obj
|
|
1106
|
+
|
|
1107
|
+
@property
|
|
1108
|
+
def scheduler(self) -> Scheduler:
|
|
1109
|
+
"""The scheduler that submissions go to from this jobscript."""
|
|
1110
|
+
if self._scheduler_obj is None:
|
|
1111
|
+
assert self.scheduler_name
|
|
1112
|
+
self._scheduler_obj = self._app.get_scheduler(
|
|
1113
|
+
scheduler_name=self.scheduler_name,
|
|
1114
|
+
os_name=self.os_name,
|
|
1115
|
+
scheduler_args=self._get_submission_scheduler_args(),
|
|
1116
|
+
)
|
|
1117
|
+
return self._scheduler_obj
|
|
1118
|
+
|
|
1119
|
+
@property
|
|
1120
|
+
def EAR_ID_file_name(self) -> str:
|
|
1121
|
+
"""
|
|
1122
|
+
The name of a file containing EAR IDs.
|
|
1123
|
+
"""
|
|
1124
|
+
return f"js_{self.index}_EAR_IDs.txt"
|
|
1125
|
+
|
|
1126
|
+
@property
|
|
1127
|
+
def combined_script_indices_file_name(self) -> str:
|
|
1128
|
+
return f"js_{self.index}_script_indices.txt"
|
|
1129
|
+
|
|
1130
|
+
@property
|
|
1131
|
+
def direct_win_pid_file_name(self) -> str:
|
|
1132
|
+
"""File for holding the direct execution PID."""
|
|
1133
|
+
return f"js_{self.index}_pid.txt"
|
|
1134
|
+
|
|
1135
|
+
@property
|
|
1136
|
+
def jobscript_name(self) -> str:
|
|
1137
|
+
"""The name of the jobscript file."""
|
|
1138
|
+
return f"js_{self.index}{self.shell.JS_EXT}"
|
|
1139
|
+
|
|
1140
|
+
@property
|
|
1141
|
+
def jobscript_functions_name(self):
|
|
1142
|
+
assert self.shell_idx is not None
|
|
1143
|
+
return self.submission.get_jobscript_functions_name(self.shell, self.shell_idx)
|
|
1144
|
+
|
|
1145
|
+
@property
|
|
1146
|
+
def EAR_ID_file_path(self) -> Path:
|
|
1147
|
+
"""
|
|
1148
|
+
The path to the file containing EAR IDs for this jobscript.
|
|
1149
|
+
"""
|
|
1150
|
+
return self.submission.js_run_ids_path / self.EAR_ID_file_name
|
|
1151
|
+
|
|
1152
|
+
@property
|
|
1153
|
+
def combined_script_indices_file_path(self) -> Path:
|
|
1154
|
+
"""
|
|
1155
|
+
The path to the file containing script indices, in the case this is a
|
|
1156
|
+
``combine_scripts=True`` jobscript.
|
|
1157
|
+
"""
|
|
1158
|
+
return (
|
|
1159
|
+
self.submission.js_script_indices_path
|
|
1160
|
+
/ self.combined_script_indices_file_name
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
@property
|
|
1164
|
+
def jobscript_path(self) -> Path:
|
|
1165
|
+
"""
|
|
1166
|
+
The path to the file containing the jobscript file.
|
|
1167
|
+
"""
|
|
1168
|
+
return self.submission.js_path / self.jobscript_name
|
|
1169
|
+
|
|
1170
|
+
@property
|
|
1171
|
+
def jobscript_functions_path(self) -> Path:
|
|
1172
|
+
"""
|
|
1173
|
+
The path to the file containing the supporting shell functions."""
|
|
1174
|
+
assert self.shell_idx is not None
|
|
1175
|
+
return self.submission.get_jobscript_functions_path(self.shell, self.shell_idx)
|
|
1176
|
+
|
|
1177
|
+
@property
|
|
1178
|
+
def std_path(self) -> Path:
|
|
1179
|
+
"""Directory in which to store jobscript standard out and error stream files."""
|
|
1180
|
+
return self.submission.js_std_path / str(self.index)
|
|
1181
|
+
|
|
1182
|
+
@property
|
|
1183
|
+
def direct_std_out_err_path(self) -> Path:
|
|
1184
|
+
"""File path of combined standard output and error streams.
|
|
1185
|
+
|
|
1186
|
+
Notes
|
|
1187
|
+
-----
|
|
1188
|
+
This path will only exist if `resources.combine_jobscript_std` is True. Otherwise,
|
|
1189
|
+
see `direct_stdout_path` and `direct_stderr_path` for the separate stream paths.
|
|
1190
|
+
|
|
1191
|
+
"""
|
|
1192
|
+
return self.get_std_out_err_path()
|
|
1193
|
+
|
|
1194
|
+
@property
|
|
1195
|
+
def direct_stdout_path(self) -> Path:
|
|
1196
|
+
"""File path to which the jobscript's standard output is saved, for direct
|
|
1197
|
+
execution only.
|
|
1198
|
+
|
|
1199
|
+
Notes
|
|
1200
|
+
-----
|
|
1201
|
+
This returned path be the same as that from `get_stderr_path` if
|
|
1202
|
+
`resources.combine_jobscript_std` is True.
|
|
1203
|
+
|
|
1204
|
+
"""
|
|
1205
|
+
assert not self.is_scheduled
|
|
1206
|
+
return self.get_stdout_path()
|
|
1207
|
+
|
|
1208
|
+
@property
|
|
1209
|
+
def direct_stderr_path(self) -> Path:
|
|
1210
|
+
"""File path to which the jobscript's standard error is saved, for direct
|
|
1211
|
+
execution only.
|
|
1212
|
+
|
|
1213
|
+
Notes
|
|
1214
|
+
-----
|
|
1215
|
+
This returned path be the same as that from `get_stdout_path` if
|
|
1216
|
+
`resources.combine_jobscript_std` is True.
|
|
1217
|
+
|
|
1218
|
+
"""
|
|
1219
|
+
assert not self.is_scheduled
|
|
1220
|
+
return self.get_stderr_path()
|
|
1221
|
+
|
|
1222
|
+
def __validate_get_std_path_array_idx(self, array_idx: int | None = None):
|
|
1223
|
+
if array_idx is None and self.is_array:
|
|
1224
|
+
raise ValueError(
|
|
1225
|
+
"`array_idx` must be specified, since this jobscript is an array job."
|
|
1226
|
+
)
|
|
1227
|
+
elif array_idx is not None and not self.is_array:
|
|
1228
|
+
raise ValueError(
|
|
1229
|
+
"`array_idx` should not be specified, since this jobscript is not an "
|
|
1230
|
+
"array job."
|
|
1231
|
+
)
|
|
1232
|
+
|
|
1233
|
+
def _get_stdout_path(self, array_idx: int | None = None) -> Path:
|
|
1234
|
+
"""File path to the separate standard output stream.
|
|
1235
|
+
|
|
1236
|
+
Notes
|
|
1237
|
+
-----
|
|
1238
|
+
This path will only exist if `resources.combine_jobscript_std` is False.
|
|
1239
|
+
Otherwise, see `get_std_out_err_path` for the combined stream path.
|
|
1240
|
+
|
|
1241
|
+
"""
|
|
1242
|
+
self.__validate_get_std_path_array_idx(array_idx)
|
|
1243
|
+
return self.std_path / self.scheduler.get_stdout_filename(
|
|
1244
|
+
js_idx=self.index, job_ID=self.scheduler_job_ID, array_idx=array_idx
|
|
1245
|
+
)
|
|
1246
|
+
|
|
1247
|
+
def _get_stderr_path(self, array_idx: int | None = None) -> Path:
|
|
1248
|
+
"""File path to the separate standard error stream.
|
|
1249
|
+
|
|
1250
|
+
Notes
|
|
1251
|
+
-----
|
|
1252
|
+
This path will only exist if `resources.combine_jobscript_std` is False.
|
|
1253
|
+
Otherwise, see `get_std_out_err_path` for the combined stream path.
|
|
1254
|
+
|
|
1255
|
+
"""
|
|
1256
|
+
self.__validate_get_std_path_array_idx(array_idx)
|
|
1257
|
+
return self.std_path / self.scheduler.get_stderr_filename(
|
|
1258
|
+
js_idx=self.index, job_ID=self.scheduler_job_ID, array_idx=array_idx
|
|
1259
|
+
)
|
|
1260
|
+
|
|
1261
|
+
def get_std_out_err_path(self, array_idx: int | None = None) -> Path:
|
|
1262
|
+
"""File path of combined standard output and error streams.
|
|
1263
|
+
|
|
1264
|
+
Notes
|
|
1265
|
+
-----
|
|
1266
|
+
This path will only exist if `resources.combine_jobscript_std` is True. Otherwise,
|
|
1267
|
+
see `get_stdout_path` and `get_stderr_path` for the separate stream paths.
|
|
1268
|
+
|
|
1269
|
+
"""
|
|
1270
|
+
self.__validate_get_std_path_array_idx(array_idx)
|
|
1271
|
+
return self.std_path / self.scheduler.get_std_out_err_filename(
|
|
1272
|
+
js_idx=self.index, job_ID=self.scheduler_job_ID, array_idx=array_idx
|
|
1273
|
+
)
|
|
1274
|
+
|
|
1275
|
+
def get_stdout_path(self, array_idx: int | None = None) -> Path:
|
|
1276
|
+
"""File path to which the jobscript's standard output is saved.
|
|
1277
|
+
|
|
1278
|
+
Notes
|
|
1279
|
+
-----
|
|
1280
|
+
This returned path be the same as that from `get_stderr_path` if
|
|
1281
|
+
`resources.combine_jobscript_std` is True.
|
|
1282
|
+
|
|
1283
|
+
"""
|
|
1284
|
+
if self.resources.combine_jobscript_std:
|
|
1285
|
+
return self.get_std_out_err_path(array_idx=array_idx)
|
|
1286
|
+
else:
|
|
1287
|
+
return self._get_stdout_path(array_idx=array_idx)
|
|
1288
|
+
|
|
1289
|
+
def get_stderr_path(self, array_idx: int | None = None) -> Path:
|
|
1290
|
+
"""File path to which the jobscript's standard error is saved.
|
|
1291
|
+
|
|
1292
|
+
Notes
|
|
1293
|
+
-----
|
|
1294
|
+
This returned path be the same as that from `get_stdout_path` if
|
|
1295
|
+
`resources.combine_jobscript_std` is True.
|
|
1296
|
+
|
|
1297
|
+
"""
|
|
1298
|
+
if self.resources.combine_jobscript_std:
|
|
1299
|
+
return self.get_std_out_err_path(array_idx=array_idx)
|
|
1300
|
+
else:
|
|
1301
|
+
return self._get_stderr_path(array_idx=array_idx)
|
|
1302
|
+
|
|
1303
|
+
def get_stdout(self, array_idx: int | None = None) -> str:
|
|
1304
|
+
"""Retrieve the contents of the standard output stream file.
|
|
1305
|
+
|
|
1306
|
+
Notes
|
|
1307
|
+
-----
|
|
1308
|
+
In the case of non-array jobscripts, this will return the whole standard output,
|
|
1309
|
+
even if that includes multiple elements/actions.
|
|
1310
|
+
|
|
1311
|
+
"""
|
|
1312
|
+
return self.workflow.get_text_file(self.get_stdout_path(array_idx))
|
|
1313
|
+
|
|
1314
|
+
def get_stderr(self, array_idx: int | None = None) -> str:
|
|
1315
|
+
"""Retrieve the contents of the standard error stream file.
|
|
1316
|
+
|
|
1317
|
+
Notes
|
|
1318
|
+
-----
|
|
1319
|
+
In the case of non-array jobscripts, this will return the whole standard error,
|
|
1320
|
+
even if that includes multiple elements/actions.
|
|
1321
|
+
|
|
1322
|
+
"""
|
|
1323
|
+
return self.workflow.get_text_file(self.get_stderr_path(array_idx))
|
|
1324
|
+
|
|
1325
|
+
def print_stdout(self, array_idx: int | None = None) -> None:
|
|
1326
|
+
"""Print the contents of the standard output stream file.
|
|
1327
|
+
|
|
1328
|
+
Notes
|
|
1329
|
+
-----
|
|
1330
|
+
In the case of non-array jobscripts, this will print the whole standard output,
|
|
1331
|
+
even if that includes multiple elements/actions.
|
|
1332
|
+
|
|
1333
|
+
"""
|
|
1334
|
+
print(self.get_stdout(array_idx))
|
|
1335
|
+
|
|
1336
|
+
def print_stderr(self, array_idx: int | None = None) -> None:
|
|
1337
|
+
"""Print the contents of the standard error stream file.
|
|
1338
|
+
|
|
1339
|
+
Notes
|
|
1340
|
+
-----
|
|
1341
|
+
In the case of non-array jobscripts, this will print the whole standard error,
|
|
1342
|
+
even if that includes multiple elements/actions.
|
|
1343
|
+
|
|
1344
|
+
"""
|
|
1345
|
+
print(self.get_stderr(array_idx))
|
|
1346
|
+
|
|
1347
|
+
@property
|
|
1348
|
+
def direct_win_pid_file_path(self) -> Path:
|
|
1349
|
+
"""
|
|
1350
|
+
The path to the file containing PIDs for directly executed commands for this
|
|
1351
|
+
jobscript. Windows only.
|
|
1352
|
+
"""
|
|
1353
|
+
return self.submission.js_win_pids_path / self.direct_win_pid_file_name
|
|
1354
|
+
|
|
1355
|
+
@property
|
|
1356
|
+
def is_scheduled(self) -> bool:
|
|
1357
|
+
return self.scheduler_name not in ("direct", "direct_posix")
|
|
1358
|
+
|
|
1359
|
+
def _update_at_submit_metadata(
|
|
1360
|
+
self,
|
|
1361
|
+
submit_cmdline: list[str] | None = None,
|
|
1362
|
+
scheduler_job_ID: str | None = None,
|
|
1363
|
+
process_ID: int | None = None,
|
|
1364
|
+
submit_time: str | None = None,
|
|
1365
|
+
):
|
|
1366
|
+
"""Update persistent store and in-memory record of at-submit metadata for this
|
|
1367
|
+
jobscript.
|
|
1368
|
+
|
|
1369
|
+
"""
|
|
1370
|
+
self.workflow._store.set_jobscript_metadata(
|
|
1371
|
+
sub_idx=self.submission.index,
|
|
1372
|
+
js_idx=self.index,
|
|
1373
|
+
submit_cmdline=submit_cmdline,
|
|
1374
|
+
scheduler_job_ID=scheduler_job_ID,
|
|
1375
|
+
process_ID=process_ID,
|
|
1376
|
+
submit_time=submit_time,
|
|
1377
|
+
)
|
|
1378
|
+
|
|
1379
|
+
if submit_cmdline is not None:
|
|
1380
|
+
self._at_submit_metadata["submit_cmdline"] = submit_cmdline
|
|
1381
|
+
if scheduler_job_ID is not None:
|
|
1382
|
+
self._at_submit_metadata["scheduler_job_ID"] = scheduler_job_ID
|
|
1383
|
+
if process_ID is not None:
|
|
1384
|
+
self._at_submit_metadata["process_ID"] = process_ID
|
|
1385
|
+
if submit_time is not None:
|
|
1386
|
+
self._at_submit_metadata["submit_time"] = submit_time
|
|
1387
|
+
|
|
1388
|
+
def _set_submit_time(self, submit_time: datetime) -> None:
|
|
1389
|
+
self._update_at_submit_metadata(
|
|
1390
|
+
submit_time=submit_time.strftime(self.workflow.ts_fmt)
|
|
1391
|
+
)
|
|
1392
|
+
|
|
1393
|
+
def _set_submit_hostname(self, submit_hostname: str) -> None:
|
|
1394
|
+
self._submit_hostname = submit_hostname
|
|
1395
|
+
self.workflow._store.set_jobscript_metadata(
|
|
1396
|
+
sub_idx=self.submission.index,
|
|
1397
|
+
js_idx=self.index,
|
|
1398
|
+
submit_hostname=submit_hostname,
|
|
1399
|
+
)
|
|
1400
|
+
|
|
1401
|
+
def _set_submit_machine(self, submit_machine: str) -> None:
|
|
1402
|
+
self._submit_machine = submit_machine
|
|
1403
|
+
self.workflow._store.set_jobscript_metadata(
|
|
1404
|
+
sub_idx=self.submission.index,
|
|
1405
|
+
js_idx=self.index,
|
|
1406
|
+
submit_machine=submit_machine,
|
|
1407
|
+
)
|
|
1408
|
+
|
|
1409
|
+
def _set_shell_idx(self, shell_idx: int) -> None:
|
|
1410
|
+
self._shell_idx = shell_idx
|
|
1411
|
+
self.workflow._store.set_jobscript_metadata(
|
|
1412
|
+
sub_idx=self.submission.index,
|
|
1413
|
+
js_idx=self.index,
|
|
1414
|
+
shell_idx=shell_idx,
|
|
1415
|
+
)
|
|
1416
|
+
|
|
1417
|
+
def _set_submit_cmdline(self, submit_cmdline: list[str]) -> None:
|
|
1418
|
+
self._update_at_submit_metadata(submit_cmdline=submit_cmdline)
|
|
1419
|
+
|
|
1420
|
+
def _set_scheduler_job_ID(self, job_ID: str) -> None:
|
|
1421
|
+
"""For scheduled submission only."""
|
|
1422
|
+
assert self.is_scheduled
|
|
1423
|
+
self._update_at_submit_metadata(scheduler_job_ID=job_ID)
|
|
1424
|
+
|
|
1425
|
+
def _set_process_ID(self, process_ID: int) -> None:
|
|
1426
|
+
"""For direct submission only."""
|
|
1427
|
+
assert not self.is_scheduled
|
|
1428
|
+
self._update_at_submit_metadata(process_ID=process_ID)
|
|
1429
|
+
|
|
1430
|
+
def _set_version_info(self, version_info: VersionInfo) -> None:
|
|
1431
|
+
self._version_info = version_info
|
|
1432
|
+
self.workflow._store.set_jobscript_metadata(
|
|
1433
|
+
sub_idx=self.submission.index,
|
|
1434
|
+
js_idx=self.index,
|
|
1435
|
+
version_info=version_info,
|
|
1436
|
+
)
|
|
1437
|
+
|
|
1438
|
+
@TimeIt.decorator
|
|
1439
|
+
def compose_jobscript(
|
|
1440
|
+
self,
|
|
1441
|
+
shell,
|
|
1442
|
+
deps: dict[int, tuple[str, bool]] | None = None,
|
|
1443
|
+
os_name: str | None = None,
|
|
1444
|
+
scheduler_name: str | None = None,
|
|
1445
|
+
scheduler_args: dict[str, Any] | None = None,
|
|
1446
|
+
) -> str:
|
|
1447
|
+
"""Prepare the jobscript file contents as a string."""
|
|
1448
|
+
scheduler_name = scheduler_name or self.scheduler_name
|
|
1449
|
+
assert scheduler_name
|
|
1450
|
+
assert os_name
|
|
1451
|
+
scheduler = self._app.get_scheduler(
|
|
1452
|
+
scheduler_name=scheduler_name,
|
|
1453
|
+
os_name=os_name,
|
|
1454
|
+
scheduler_args=scheduler_args or self._get_submission_scheduler_args(),
|
|
1455
|
+
)
|
|
1456
|
+
app_caps = self._app.package_name.upper()
|
|
1457
|
+
header_args = {
|
|
1458
|
+
"app_caps": app_caps,
|
|
1459
|
+
"jobscript_functions_name": self.jobscript_functions_name,
|
|
1460
|
+
"jobscript_functions_dir": self.submission.JS_FUNCS_DIR_NAME,
|
|
1461
|
+
"sub_idx": self.submission.index,
|
|
1462
|
+
"js_idx": self.index,
|
|
1463
|
+
"run_IDs_file_name": self.EAR_ID_file_name,
|
|
1464
|
+
"run_IDs_file_dir": self.submission.JS_RUN_IDS_DIR_NAME,
|
|
1465
|
+
"tmp_dir_name": self.submission.TMP_DIR_NAME,
|
|
1466
|
+
"log_dir_name": self.submission.LOG_DIR_NAME,
|
|
1467
|
+
"app_std_dir_name": self.submission.APP_STD_DIR_NAME,
|
|
1468
|
+
"scripts_dir_name": self.submission.SCRIPTS_DIR_NAME,
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
shebang = shell.JS_SHEBANG.format(
|
|
1472
|
+
shebang=" ".join(scheduler.shebang_executable or shell.shebang_executable)
|
|
1473
|
+
)
|
|
1474
|
+
header = shell.JS_HEADER.format(**header_args)
|
|
1475
|
+
|
|
1476
|
+
if isinstance(scheduler, QueuedScheduler):
|
|
1477
|
+
header = shell.JS_SCHEDULER_HEADER.format(
|
|
1478
|
+
shebang=shebang,
|
|
1479
|
+
scheduler_options=scheduler.format_directives(
|
|
1480
|
+
resources=self.resources,
|
|
1481
|
+
num_elements=self.blocks[0].num_elements, # only used for array jobs
|
|
1482
|
+
is_array=self.is_array,
|
|
1483
|
+
sub_idx=self.submission.index,
|
|
1484
|
+
js_idx=self.index,
|
|
1485
|
+
),
|
|
1486
|
+
header=header,
|
|
1487
|
+
)
|
|
1488
|
+
else:
|
|
1489
|
+
# the Scheduler (direct submission)
|
|
1490
|
+
assert isinstance(scheduler, DirectScheduler)
|
|
1491
|
+
wait_cmd = shell.get_wait_command(
|
|
1492
|
+
workflow_app_alias=self.workflow_app_alias,
|
|
1493
|
+
sub_idx=self.submission.index,
|
|
1494
|
+
deps=deps or {},
|
|
1495
|
+
)
|
|
1496
|
+
header = shell.JS_DIRECT_HEADER.format(
|
|
1497
|
+
shebang=shebang,
|
|
1498
|
+
header=header,
|
|
1499
|
+
workflow_app_alias=self.workflow_app_alias,
|
|
1500
|
+
wait_command=wait_cmd,
|
|
1501
|
+
)
|
|
1502
|
+
|
|
1503
|
+
out = header
|
|
1504
|
+
|
|
1505
|
+
if self.resources.combine_scripts:
|
|
1506
|
+
run_cmd = shell.JS_RUN_CMD_COMBINED.format(
|
|
1507
|
+
workflow_app_alias=self.workflow_app_alias
|
|
1508
|
+
)
|
|
1509
|
+
out += run_cmd + "\n"
|
|
1510
|
+
else:
|
|
1511
|
+
run_cmd = shell.JS_RUN_CMD.format(workflow_app_alias=self.workflow_app_alias)
|
|
1512
|
+
|
|
1513
|
+
if self.resources.write_app_logs:
|
|
1514
|
+
run_log_enable_disable = shell.JS_RUN_LOG_PATH_ENABLE.format(
|
|
1515
|
+
run_log_file_name=self.submission.get_app_log_file_name(
|
|
1516
|
+
run_ID=shell.format_env_var_get(f"{app_caps}_RUN_ID")
|
|
1517
|
+
)
|
|
1518
|
+
)
|
|
1519
|
+
else:
|
|
1520
|
+
run_log_enable_disable = shell.JS_RUN_LOG_PATH_DISABLE
|
|
1521
|
+
|
|
1522
|
+
block_run = shell.JS_RUN.format(
|
|
1523
|
+
EAR_files_delimiter=self._EAR_files_delimiter,
|
|
1524
|
+
app_caps=app_caps,
|
|
1525
|
+
run_cmd=run_cmd,
|
|
1526
|
+
sub_tmp_dir=self.submission.tmp_path,
|
|
1527
|
+
run_log_enable_disable=run_log_enable_disable,
|
|
1528
|
+
)
|
|
1529
|
+
if len(self.blocks) == 1:
|
|
1530
|
+
# forgo element and action loops if not necessary:
|
|
1531
|
+
block = self.blocks[0]
|
|
1532
|
+
if block.num_actions > 1:
|
|
1533
|
+
block_act = shell.JS_ACT_MULTI.format(
|
|
1534
|
+
num_actions=block.num_actions,
|
|
1535
|
+
run_block=indent(block_run, shell.JS_INDENT),
|
|
1536
|
+
)
|
|
1537
|
+
else:
|
|
1538
|
+
block_act = shell.JS_ACT_SINGLE.format(run_block=block_run)
|
|
1539
|
+
|
|
1540
|
+
main = shell.JS_MAIN.format(
|
|
1541
|
+
action=block_act,
|
|
1542
|
+
app_caps=app_caps,
|
|
1543
|
+
block_start_elem_idx=0,
|
|
1544
|
+
)
|
|
1545
|
+
|
|
1546
|
+
out += shell.JS_BLOCK_HEADER.format(app_caps=app_caps)
|
|
1547
|
+
if self.is_array:
|
|
1548
|
+
if not isinstance(scheduler, QueuedScheduler):
|
|
1549
|
+
raise Exception("can only schedule arrays of jobs to a queue")
|
|
1550
|
+
out += shell.JS_ELEMENT_MULTI_ARRAY.format(
|
|
1551
|
+
scheduler_command=scheduler.js_cmd,
|
|
1552
|
+
scheduler_array_switch=scheduler.array_switch,
|
|
1553
|
+
scheduler_array_item_var=scheduler.array_item_var,
|
|
1554
|
+
num_elements=block.num_elements,
|
|
1555
|
+
main=main,
|
|
1556
|
+
)
|
|
1557
|
+
elif block.num_elements == 1:
|
|
1558
|
+
out += shell.JS_ELEMENT_SINGLE.format(
|
|
1559
|
+
block_start_elem_idx=0,
|
|
1560
|
+
main=main,
|
|
1561
|
+
)
|
|
1562
|
+
else:
|
|
1563
|
+
out += shell.JS_ELEMENT_MULTI_LOOP.format(
|
|
1564
|
+
block_start_elem_idx=0,
|
|
1565
|
+
num_elements=block.num_elements,
|
|
1566
|
+
main=indent(main, shell.JS_INDENT),
|
|
1567
|
+
)
|
|
1568
|
+
|
|
1569
|
+
else:
|
|
1570
|
+
# use a shell loop for blocks, so always write the inner element and action
|
|
1571
|
+
# loops:
|
|
1572
|
+
block_act = shell.JS_ACT_MULTI.format(
|
|
1573
|
+
num_actions=shell.format_array_get_item("num_actions", "$block_idx"),
|
|
1574
|
+
run_block=indent(block_run, shell.JS_INDENT),
|
|
1575
|
+
)
|
|
1576
|
+
main = shell.JS_MAIN.format(
|
|
1577
|
+
action=block_act,
|
|
1578
|
+
app_caps=app_caps,
|
|
1579
|
+
block_start_elem_idx="$block_start_elem_idx",
|
|
1580
|
+
)
|
|
1581
|
+
|
|
1582
|
+
# only non-array jobscripts will have multiple blocks:
|
|
1583
|
+
element_loop = shell.JS_ELEMENT_MULTI_LOOP.format(
|
|
1584
|
+
block_start_elem_idx="$block_start_elem_idx",
|
|
1585
|
+
num_elements=shell.format_array_get_item(
|
|
1586
|
+
"num_elements", "$block_idx"
|
|
1587
|
+
),
|
|
1588
|
+
main=indent(main, shell.JS_INDENT),
|
|
1589
|
+
)
|
|
1590
|
+
out += shell.JS_BLOCK_LOOP.format(
|
|
1591
|
+
num_elements=shell.format_array(
|
|
1592
|
+
[i.num_elements for i in self.blocks]
|
|
1593
|
+
),
|
|
1594
|
+
num_actions=shell.format_array([i.num_actions for i in self.blocks]),
|
|
1595
|
+
num_blocks=len(self.blocks),
|
|
1596
|
+
app_caps=app_caps,
|
|
1597
|
+
element_loop=indent(element_loop, shell.JS_INDENT),
|
|
1598
|
+
)
|
|
1599
|
+
|
|
1600
|
+
out += shell.JS_FOOTER
|
|
1601
|
+
|
|
1602
|
+
return out
|
|
1603
|
+
|
|
1604
|
+
@TimeIt.decorator
|
|
1605
|
+
def write_jobscript(
|
|
1606
|
+
self,
|
|
1607
|
+
os_name: str | None = None,
|
|
1608
|
+
shell_name: str | None = None,
|
|
1609
|
+
deps: dict[int, tuple[str, bool]] | None = None,
|
|
1610
|
+
os_args: dict[str, Any] | None = None,
|
|
1611
|
+
shell_args: dict[str, Any] | None = None,
|
|
1612
|
+
scheduler_name: str | None = None,
|
|
1613
|
+
scheduler_args: dict[str, Any] | None = None,
|
|
1614
|
+
) -> Path:
|
|
1615
|
+
"""
|
|
1616
|
+
Write the jobscript to its file.
|
|
1617
|
+
"""
|
|
1618
|
+
os_name = os_name or self.os_name
|
|
1619
|
+
shell_name = shell_name or self.shell_name
|
|
1620
|
+
assert os_name
|
|
1621
|
+
assert shell_name
|
|
1622
|
+
shell = self._get_shell(
|
|
1623
|
+
os_name=os_name,
|
|
1624
|
+
shell_name=shell_name,
|
|
1625
|
+
os_args=os_args or self._get_submission_os_args(),
|
|
1626
|
+
shell_args=shell_args or self._get_submission_shell_args(),
|
|
1627
|
+
)
|
|
1628
|
+
|
|
1629
|
+
js_str = self.compose_jobscript(
|
|
1630
|
+
deps=deps,
|
|
1631
|
+
shell=shell,
|
|
1632
|
+
os_name=os_name,
|
|
1633
|
+
scheduler_name=scheduler_name,
|
|
1634
|
+
scheduler_args=scheduler_args,
|
|
1635
|
+
)
|
|
1636
|
+
with self.jobscript_path.open("wt", newline="\n") as fp:
|
|
1637
|
+
fp.write(js_str)
|
|
1638
|
+
|
|
1639
|
+
return self.jobscript_path
|
|
1640
|
+
|
|
1641
|
+
@TimeIt.decorator
|
|
1642
|
+
def _launch_direct_js_win(self, submit_cmd: list[str]) -> int:
|
|
1643
|
+
# this is a "trick" to ensure we always get a fully detached new process (with no
|
|
1644
|
+
# parent); the `powershell.exe -Command` process exits after running the inner
|
|
1645
|
+
# `Start-Process`, which is where the jobscript is actually invoked. I could not
|
|
1646
|
+
# find a way using `subprocess.Popen()` to ensure the new process was fully
|
|
1647
|
+
# detached when submitting jobscripts via a Jupyter notebook in Windows.
|
|
1648
|
+
|
|
1649
|
+
# Note we need powershell.exe for this "launcher process", but the shell used for
|
|
1650
|
+
# the jobscript itself need not be powershell.exe
|
|
1651
|
+
exe_path, arg_list = submit_cmd[0], submit_cmd[1:]
|
|
1652
|
+
|
|
1653
|
+
# note powershell-escaped quotes, in case of spaces in arguments (this seems to
|
|
1654
|
+
# work okay even though we might have switch like arguments in this list, like
|
|
1655
|
+
# "-File"):
|
|
1656
|
+
arg_list_str = ",".join(f'"`"{i}`""' for i in arg_list)
|
|
1657
|
+
|
|
1658
|
+
args = [
|
|
1659
|
+
"powershell.exe",
|
|
1660
|
+
"-Command",
|
|
1661
|
+
f"$JS_proc = Start-Process "
|
|
1662
|
+
f'-Passthru -NoNewWindow -FilePath "{exe_path}" '
|
|
1663
|
+
f'-RedirectStandardOutput "{self.direct_stdout_path}" '
|
|
1664
|
+
f'-RedirectStandardError "{self.direct_stderr_path}" '
|
|
1665
|
+
f'-WorkingDirectory "{self.workflow.path}" '
|
|
1666
|
+
f"-ArgumentList {arg_list_str}; "
|
|
1667
|
+
f'Set-Content -Path "{self.direct_win_pid_file_path}" -Value $JS_proc.Id',
|
|
1668
|
+
]
|
|
1669
|
+
|
|
1670
|
+
self._app.submission_logger.info(
|
|
1671
|
+
f"running direct Windows jobscript launcher process: {args!r}"
|
|
1672
|
+
)
|
|
1673
|
+
# for some reason we still need to create a "detached" process here as well:
|
|
1674
|
+
init_proc = subprocess.Popen(
|
|
1675
|
+
args=args,
|
|
1676
|
+
cwd=self.workflow.path,
|
|
1677
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
1678
|
+
)
|
|
1679
|
+
init_proc.wait() # wait for the process ID file to be written
|
|
1680
|
+
return int(self.direct_win_pid_file_path.read_text())
|
|
1681
|
+
|
|
1682
|
+
@TimeIt.decorator
|
|
1683
|
+
def _launch_direct_js_posix(self, submit_cmd: list[str]) -> int:
|
|
1684
|
+
# direct submission; submit jobscript asynchronously:
|
|
1685
|
+
# detached process, avoid interrupt signals propagating to the subprocess:
|
|
1686
|
+
|
|
1687
|
+
def _launch(fp_stdout: TextIO, fp_stderr: TextIO) -> int:
|
|
1688
|
+
# note: Popen copies the file objects, so this works!
|
|
1689
|
+
proc = subprocess.Popen(
|
|
1690
|
+
args=submit_cmd,
|
|
1691
|
+
stdout=fp_stdout,
|
|
1692
|
+
stderr=fp_stderr,
|
|
1693
|
+
cwd=str(self.workflow.path),
|
|
1694
|
+
start_new_session=True,
|
|
1695
|
+
)
|
|
1696
|
+
return proc.pid
|
|
1697
|
+
|
|
1698
|
+
if self.resources.combine_jobscript_std:
|
|
1699
|
+
with self.direct_std_out_err_path.open("wt") as fp_std:
|
|
1700
|
+
return _launch(fp_std, fp_std)
|
|
1701
|
+
else:
|
|
1702
|
+
with self.direct_stdout_path.open(
|
|
1703
|
+
"wt"
|
|
1704
|
+
) as fp_stdout, self.direct_stderr_path.open("wt") as fp_stderr:
|
|
1705
|
+
return _launch(fp_stdout, fp_stderr)
|
|
1706
|
+
|
|
1707
|
+
@TimeIt.decorator
|
|
1708
|
+
def _launch_queued(
|
|
1709
|
+
self, submit_cmd: list[str], print_stdout: bool
|
|
1710
|
+
) -> tuple[str, str]:
|
|
1711
|
+
# scheduled submission, wait for submission so we can parse the job ID:
|
|
1712
|
+
proc = subprocess.run(
|
|
1713
|
+
args=submit_cmd,
|
|
1714
|
+
stdout=subprocess.PIPE,
|
|
1715
|
+
stderr=subprocess.PIPE,
|
|
1716
|
+
cwd=self.workflow.path,
|
|
1717
|
+
)
|
|
1718
|
+
stdout = proc.stdout.decode().strip()
|
|
1719
|
+
stderr = proc.stderr.decode().strip()
|
|
1720
|
+
if print_stdout and stdout:
|
|
1721
|
+
print(stdout)
|
|
1722
|
+
if stderr:
|
|
1723
|
+
print(stderr)
|
|
1724
|
+
return stdout, stderr
|
|
1725
|
+
|
|
1726
|
+
@TimeIt.decorator
|
|
1727
|
+
def submit(
|
|
1728
|
+
self,
|
|
1729
|
+
scheduler_refs: dict[int, tuple[str, bool]],
|
|
1730
|
+
print_stdout: bool = False,
|
|
1731
|
+
) -> str:
|
|
1732
|
+
"""
|
|
1733
|
+
Submit the jobscript to the scheduler.
|
|
1734
|
+
"""
|
|
1735
|
+
# map each dependency jobscript index to the JS ref (job/process ID) and if the
|
|
1736
|
+
# dependency is an array dependency:
|
|
1737
|
+
deps: dict[int, tuple[str, bool]] = {}
|
|
1738
|
+
for (js_idx, _), deps_i in self.dependencies.items():
|
|
1739
|
+
dep_js_ref, dep_js_is_arr = scheduler_refs[js_idx]
|
|
1740
|
+
# only submit an array dependency if both this jobscript and the dependency
|
|
1741
|
+
# are array jobs:
|
|
1742
|
+
dep_is_arr = deps_i["is_array"] and self.is_array and dep_js_is_arr
|
|
1743
|
+
deps[js_idx] = (dep_js_ref, dep_is_arr)
|
|
1744
|
+
|
|
1745
|
+
if self.index > 0:
|
|
1746
|
+
# prevent this jobscript executing if jobscript parallelism is not available:
|
|
1747
|
+
use_parallelism = (
|
|
1748
|
+
self.submission.JS_parallelism is True
|
|
1749
|
+
or {0: "direct", 1: "scheduled"}[self.is_scheduled]
|
|
1750
|
+
== self.submission.JS_parallelism
|
|
1751
|
+
)
|
|
1752
|
+
if not use_parallelism:
|
|
1753
|
+
# add fake dependencies to all previously submitted jobscripts to avoid
|
|
1754
|
+
# simultaneous execution:
|
|
1755
|
+
for js_idx, (js_ref, _) in scheduler_refs.items():
|
|
1756
|
+
if js_idx not in deps:
|
|
1757
|
+
deps[js_idx] = (js_ref, False)
|
|
1758
|
+
|
|
1759
|
+
# make directory for jobscripts stdout/err stream files:
|
|
1760
|
+
self.std_path.mkdir(exist_ok=True)
|
|
1761
|
+
|
|
1762
|
+
with self.EAR_ID_file_path.open(mode="wt", newline="\n") as ID_fp:
|
|
1763
|
+
for block in self.blocks:
|
|
1764
|
+
block.write_EAR_ID_file(ID_fp)
|
|
1765
|
+
|
|
1766
|
+
js_path = self.shell.prepare_JS_path(self.write_jobscript(deps=deps))
|
|
1767
|
+
submit_cmd = self.scheduler.get_submit_command(self.shell, js_path, deps)
|
|
1768
|
+
self._app.submission_logger.info(
|
|
1769
|
+
f"submitting jobscript {self.index!r} with command: {submit_cmd!r}"
|
|
1770
|
+
)
|
|
1771
|
+
|
|
1772
|
+
err_args: JobscriptSubmissionFailureArgs = {
|
|
1773
|
+
"submit_cmd": submit_cmd,
|
|
1774
|
+
"js_idx": self.index,
|
|
1775
|
+
"js_path": js_path,
|
|
1776
|
+
}
|
|
1777
|
+
job_ID: str | None = None
|
|
1778
|
+
process_ID: int | None = None
|
|
1779
|
+
try:
|
|
1780
|
+
if isinstance(self.scheduler, QueuedScheduler):
|
|
1781
|
+
# scheduled submission, wait for submission so we can parse the job ID:
|
|
1782
|
+
stdout, stderr = self._launch_queued(submit_cmd, print_stdout)
|
|
1783
|
+
err_args["stdout"] = stdout
|
|
1784
|
+
err_args["stderr"] = stderr
|
|
1785
|
+
else:
|
|
1786
|
+
if os.name == "nt":
|
|
1787
|
+
process_ID = self._launch_direct_js_win(submit_cmd)
|
|
1788
|
+
else:
|
|
1789
|
+
process_ID = self._launch_direct_js_posix(submit_cmd)
|
|
1790
|
+
except Exception as subprocess_exc:
|
|
1791
|
+
err_args["subprocess_exc"] = subprocess_exc
|
|
1792
|
+
raise JobscriptSubmissionFailure(
|
|
1793
|
+
"Failed to execute submit command.", **err_args
|
|
1794
|
+
)
|
|
1795
|
+
|
|
1796
|
+
if isinstance(self.scheduler, QueuedScheduler):
|
|
1797
|
+
# scheduled submission
|
|
1798
|
+
if stderr:
|
|
1799
|
+
raise JobscriptSubmissionFailure(
|
|
1800
|
+
"Non-empty stderr from submit command.", **err_args
|
|
1801
|
+
)
|
|
1802
|
+
|
|
1803
|
+
try:
|
|
1804
|
+
job_ID = self.scheduler.parse_submission_output(stdout)
|
|
1805
|
+
assert job_ID is not None
|
|
1806
|
+
except Exception as job_ID_parse_exc:
|
|
1807
|
+
# TODO: maybe handle this differently. If there is no stderr, then the job
|
|
1808
|
+
# probably did submit fine, but the issue is just with parsing the job ID
|
|
1809
|
+
# (e.g. if the scheduler version was updated and it now outputs
|
|
1810
|
+
# differently).
|
|
1811
|
+
err_args["job_ID_parse_exc"] = job_ID_parse_exc
|
|
1812
|
+
raise JobscriptSubmissionFailure(
|
|
1813
|
+
"Failed to parse job ID from stdout.", **err_args
|
|
1814
|
+
)
|
|
1815
|
+
|
|
1816
|
+
self._set_scheduler_job_ID(job_ID)
|
|
1817
|
+
ref = job_ID
|
|
1818
|
+
|
|
1819
|
+
else:
|
|
1820
|
+
# direct submission
|
|
1821
|
+
assert process_ID is not None
|
|
1822
|
+
self._set_process_ID(process_ID)
|
|
1823
|
+
ref = str(process_ID)
|
|
1824
|
+
|
|
1825
|
+
self._set_submit_cmdline(submit_cmd)
|
|
1826
|
+
self._set_submit_time(current_timestamp())
|
|
1827
|
+
|
|
1828
|
+
# a downstream direct jobscript might need to wait for this jobscript, which
|
|
1829
|
+
# means this jobscript's process ID must be committed:
|
|
1830
|
+
self.workflow._store._pending.commit_all()
|
|
1831
|
+
|
|
1832
|
+
return ref
|
|
1833
|
+
|
|
1834
|
+
@property
|
|
1835
|
+
def is_submitted(self) -> bool:
|
|
1836
|
+
"""Whether this jobscript has been submitted."""
|
|
1837
|
+
return self.index in self.submission.submitted_jobscripts
|
|
1838
|
+
|
|
1839
|
+
@property
|
|
1840
|
+
def scheduler_js_ref(self) -> str | None | tuple[int | None, list[str] | None]:
|
|
1841
|
+
"""
|
|
1842
|
+
The reference to the submitted job for the jobscript.
|
|
1843
|
+
"""
|
|
1844
|
+
if isinstance(self.scheduler, QueuedScheduler):
|
|
1845
|
+
return self.scheduler_job_ID
|
|
1846
|
+
else:
|
|
1847
|
+
return (self.process_ID, self.submit_cmdline)
|
|
1848
|
+
|
|
1849
|
+
@overload
|
|
1850
|
+
def get_active_states(
|
|
1851
|
+
self, as_json: Literal[False] = False
|
|
1852
|
+
) -> Mapping[int, Mapping[int, JobscriptElementState]]: ...
|
|
1853
|
+
|
|
1854
|
+
@overload
|
|
1855
|
+
def get_active_states(
|
|
1856
|
+
self, as_json: Literal[True]
|
|
1857
|
+
) -> Mapping[int, Mapping[int, str]]: ...
|
|
1858
|
+
|
|
1859
|
+
@TimeIt.decorator
|
|
1860
|
+
def get_active_states(
|
|
1861
|
+
self, as_json: bool = False
|
|
1862
|
+
) -> Mapping[int, Mapping[int, JobscriptElementState | str]]:
|
|
1863
|
+
"""If this jobscript is active on this machine, return the state information from
|
|
1864
|
+
the scheduler."""
|
|
1865
|
+
# this returns: {BLOCK_IDX: {JS_ELEMENT_IDX: STATE}}
|
|
1866
|
+
out: Mapping[int, Mapping[int, JobscriptElementState]] = {}
|
|
1867
|
+
if self.is_submitted:
|
|
1868
|
+
self._app.submission_logger.debug(
|
|
1869
|
+
"checking if the jobscript is running according to EAR submission "
|
|
1870
|
+
"states."
|
|
1871
|
+
)
|
|
1872
|
+
|
|
1873
|
+
not_run_states = EARStatus.get_non_running_submitted_states()
|
|
1874
|
+
all_EAR_states = set(ear.status for ear in self.all_EARs)
|
|
1875
|
+
self._app.submission_logger.debug(
|
|
1876
|
+
f"Unique EAR states are: {tuple(i.name for i in all_EAR_states)!r}"
|
|
1877
|
+
)
|
|
1878
|
+
if all_EAR_states.issubset(not_run_states):
|
|
1879
|
+
self._app.submission_logger.debug(
|
|
1880
|
+
"All jobscript EARs are in a non-running state"
|
|
1881
|
+
)
|
|
1882
|
+
|
|
1883
|
+
elif self._app.config.get("machine") == self.submit_machine:
|
|
1884
|
+
self._app.submission_logger.debug(
|
|
1885
|
+
"Checking if jobscript is running according to the scheduler/process "
|
|
1886
|
+
"ID."
|
|
1887
|
+
)
|
|
1888
|
+
out_d = self.scheduler.get_job_state_info(js_refs=[self.scheduler_js_ref])
|
|
1889
|
+
if out_d:
|
|
1890
|
+
# remove scheduler ref (should be only one):
|
|
1891
|
+
assert len(out_d) == 1
|
|
1892
|
+
out_i = nth_value(cast("dict", out_d), 0)
|
|
1893
|
+
|
|
1894
|
+
if self.is_array:
|
|
1895
|
+
# out_i is a dict keyed by array index; there will be exactly one
|
|
1896
|
+
# block:
|
|
1897
|
+
out = {0: out_i}
|
|
1898
|
+
else:
|
|
1899
|
+
# out_i is a single state:
|
|
1900
|
+
out = {
|
|
1901
|
+
idx: {i: out_i for i in range(block.num_elements)}
|
|
1902
|
+
for idx, block in enumerate(self.blocks)
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
else:
|
|
1906
|
+
raise NotSubmitMachineError()
|
|
1907
|
+
|
|
1908
|
+
self._app.submission_logger.info(f"Jobscript is {'in' if not out else ''}active.")
|
|
1909
|
+
if as_json:
|
|
1910
|
+
return {
|
|
1911
|
+
block_idx: {k: v.name for k, v in block_data.items()}
|
|
1912
|
+
for block_idx, block_data in out.items()
|
|
1913
|
+
}
|
|
1914
|
+
return out
|
|
1915
|
+
|
|
1916
|
+
def compose_combined_script(
|
|
1917
|
+
self, action_scripts: list[list[tuple[str, Path, bool]]]
|
|
1918
|
+
) -> tuple[str, list[list[int]], list[int], list[int]]:
|
|
1919
|
+
"""
|
|
1920
|
+
Prepare the combined-script file string, if applicable.
|
|
1921
|
+
"""
|
|
1922
|
+
|
|
1923
|
+
# use an index array for action scripts:
|
|
1924
|
+
script_names: list[str] = []
|
|
1925
|
+
requires_dir: list[bool] = []
|
|
1926
|
+
script_data: dict[str, tuple[int, Path]] = {}
|
|
1927
|
+
script_indices: list[list[int]] = []
|
|
1928
|
+
for i in action_scripts:
|
|
1929
|
+
indices_i: list[int] = []
|
|
1930
|
+
for name_j, path_j, req_dir_i in i:
|
|
1931
|
+
if name_j in script_data:
|
|
1932
|
+
idx = script_data[name_j][0]
|
|
1933
|
+
else:
|
|
1934
|
+
idx = len(script_names)
|
|
1935
|
+
script_names.append(name_j)
|
|
1936
|
+
requires_dir.append(req_dir_i)
|
|
1937
|
+
script_data[name_j] = (idx, path_j)
|
|
1938
|
+
indices_i.append(idx)
|
|
1939
|
+
script_indices.append(indices_i)
|
|
1940
|
+
|
|
1941
|
+
if not self.resources.combine_scripts:
|
|
1942
|
+
raise TypeError(
|
|
1943
|
+
f"Jobscript {self.index} is not a `combine_scripts` jobscript."
|
|
1944
|
+
)
|
|
1945
|
+
|
|
1946
|
+
tab_indent = " "
|
|
1947
|
+
|
|
1948
|
+
script_funcs_lst: list[str] = []
|
|
1949
|
+
future_imports: set[str] = set()
|
|
1950
|
+
for act_name, (_, snip_path) in script_data.items():
|
|
1951
|
+
main_func_name = snip_path.stem
|
|
1952
|
+
with snip_path.open("rt") as fp:
|
|
1953
|
+
script_str = fp.read()
|
|
1954
|
+
script_str, future_imports_i = extract_py_from_future_imports(script_str)
|
|
1955
|
+
future_imports.update(future_imports_i)
|
|
1956
|
+
script_funcs_lst.append(
|
|
1957
|
+
dedent(
|
|
1958
|
+
"""\
|
|
1959
|
+
def {act_name}(*args, **kwargs):
|
|
1960
|
+
{script_str}
|
|
1961
|
+
return {main_func_name}(*args, **kwargs)
|
|
1962
|
+
"""
|
|
1963
|
+
).format(
|
|
1964
|
+
act_name=act_name,
|
|
1965
|
+
script_str=indent(script_str, tab_indent),
|
|
1966
|
+
main_func_name=main_func_name,
|
|
1967
|
+
)
|
|
1968
|
+
)
|
|
1969
|
+
|
|
1970
|
+
app_caps = self._app.package_name.upper()
|
|
1971
|
+
if self.resources.write_app_logs:
|
|
1972
|
+
sub_log_path = f'os.environ["{app_caps}_LOG_PATH"]'
|
|
1973
|
+
else:
|
|
1974
|
+
sub_log_path = '""'
|
|
1975
|
+
|
|
1976
|
+
py_imports = dedent(
|
|
1977
|
+
"""\
|
|
1978
|
+
import os
|
|
1979
|
+
from collections import defaultdict
|
|
1980
|
+
from pathlib import Path
|
|
1981
|
+
import traceback
|
|
1982
|
+
import time
|
|
1983
|
+
from typing import Dict
|
|
1984
|
+
|
|
1985
|
+
import {app_module} as app
|
|
1986
|
+
|
|
1987
|
+
from hpcflow.sdk.core.errors import UnsetParameterDataErrorBase
|
|
1988
|
+
|
|
1989
|
+
log_path = {log_path}
|
|
1990
|
+
wk_path = os.getenv("{app_caps}_WK_PATH")
|
|
1991
|
+
"""
|
|
1992
|
+
).format(
|
|
1993
|
+
app_module=self._app.module,
|
|
1994
|
+
app_caps=app_caps,
|
|
1995
|
+
log_path=sub_log_path,
|
|
1996
|
+
)
|
|
1997
|
+
|
|
1998
|
+
py_main_block_workflow_load = dedent(
|
|
1999
|
+
"""\
|
|
2000
|
+
app.load_config(
|
|
2001
|
+
log_file_path=log_path,
|
|
2002
|
+
config_dir=r"{cfg_dir}",
|
|
2003
|
+
config_key=r"{cfg_invoc_key}",
|
|
2004
|
+
)
|
|
2005
|
+
wk = app.Workflow(wk_path)
|
|
2006
|
+
"""
|
|
2007
|
+
).format(
|
|
2008
|
+
cfg_dir=self._app.config.config_directory,
|
|
2009
|
+
cfg_invoc_key=self._app.config.config_key,
|
|
2010
|
+
app_caps=app_caps,
|
|
2011
|
+
)
|
|
2012
|
+
|
|
2013
|
+
func_invoc_lines = dedent(
|
|
2014
|
+
"""\
|
|
2015
|
+
import pprint
|
|
2016
|
+
if not run.action.is_OFP and run.action.script_data_out_has_direct:
|
|
2017
|
+
outputs = func(**func_kwargs)
|
|
2018
|
+
elif run.action.is_OFP:
|
|
2019
|
+
out_name = run.action.output_file_parsers[0].output.typ
|
|
2020
|
+
outputs = {out_name: func(**func_kwargs)}
|
|
2021
|
+
else:
|
|
2022
|
+
outputs = {}
|
|
2023
|
+
func(**func_kwargs)
|
|
2024
|
+
"""
|
|
2025
|
+
)
|
|
2026
|
+
|
|
2027
|
+
script_funcs = "\n".join(script_funcs_lst)
|
|
2028
|
+
script_names_str = "[" + ", ".join(f"{i}" for i in script_names) + "]"
|
|
2029
|
+
main = dedent(
|
|
2030
|
+
"""\
|
|
2031
|
+
{py_imports}
|
|
2032
|
+
|
|
2033
|
+
sub_std_path = Path(os.environ["{app_caps}_SUB_STD_DIR"], f"js_{js_idx}.txt")
|
|
2034
|
+
with app.redirect_std_to_file(sub_std_path):
|
|
2035
|
+
{py_main_block_workflow_load}
|
|
2036
|
+
|
|
2037
|
+
with open(os.environ["{app_caps}_RUN_ID_FILE"], mode="r") as fp:
|
|
2038
|
+
lns = fp.read().strip().split("\\n")
|
|
2039
|
+
run_IDs = [[int(i) for i in ln.split("{run_ID_delim}")] for ln in lns]
|
|
2040
|
+
|
|
2041
|
+
get_all_runs_tic = time.perf_counter()
|
|
2042
|
+
run_IDs_flat = [j for i in run_IDs for j in i]
|
|
2043
|
+
runs = wk.get_EARs_from_IDs(run_IDs_flat, as_dict=True)
|
|
2044
|
+
run_skips : Dict[int, bool] = {{k: v.skip for k, v in runs.items()}}
|
|
2045
|
+
get_all_runs_toc = time.perf_counter()
|
|
2046
|
+
|
|
2047
|
+
with open(os.environ["{app_caps}_SCRIPT_INDICES_FILE"], mode="r") as fp:
|
|
2048
|
+
lns = fp.read().strip().split("\\n")
|
|
2049
|
+
section_idx = -1
|
|
2050
|
+
script_indices = []
|
|
2051
|
+
for ln in lns:
|
|
2052
|
+
if ln.startswith("#"):
|
|
2053
|
+
section_idx += 1
|
|
2054
|
+
continue
|
|
2055
|
+
ln_parsed = [int(i) for i in ln.split("{script_idx_delim}")]
|
|
2056
|
+
if section_idx == 0:
|
|
2057
|
+
num_elements = ln_parsed
|
|
2058
|
+
elif section_idx == 1:
|
|
2059
|
+
num_actions = ln_parsed
|
|
2060
|
+
else:
|
|
2061
|
+
script_indices.append(ln_parsed)
|
|
2062
|
+
|
|
2063
|
+
port = int(os.environ["{app_caps}_RUN_PORT"])
|
|
2064
|
+
action_scripts = {script_names}
|
|
2065
|
+
requires_dir = {requires_dir!r}
|
|
2066
|
+
run_dirs = wk.get_run_directories()
|
|
2067
|
+
|
|
2068
|
+
get_ins_time_fp = open(f"js_{js_idx}_get_inputs_times.txt", "wt")
|
|
2069
|
+
func_time_fp = open(f"js_{js_idx}_func_times.txt", "wt")
|
|
2070
|
+
run_time_fp = open(f"js_{js_idx}_run_times.txt", "wt")
|
|
2071
|
+
set_start_multi_times_fp = open(f"js_{js_idx}_set_start_multi_times.txt", "wt")
|
|
2072
|
+
set_end_multi_times_fp = open(f"js_{js_idx}_set_end_multi_times.txt", "wt")
|
|
2073
|
+
save_multi_times_fp = open(f"js_{js_idx}_save_multi_times.txt", "wt")
|
|
2074
|
+
loop_term_times_fp = open(f"js_{js_idx}_loop_term_times.txt", "wt")
|
|
2075
|
+
|
|
2076
|
+
get_all_runs_time = get_all_runs_toc - get_all_runs_tic
|
|
2077
|
+
print(f"get_all_runs_time: {{get_all_runs_time:.4f}}")
|
|
2078
|
+
|
|
2079
|
+
app.logger.info(
|
|
2080
|
+
f"running {num_blocks} jobscript block(s) in combined jobscript index "
|
|
2081
|
+
f"{js_idx}."
|
|
2082
|
+
)
|
|
2083
|
+
|
|
2084
|
+
block_start_elem_idx = 0
|
|
2085
|
+
for block_idx in range({num_blocks}):
|
|
2086
|
+
|
|
2087
|
+
app.logger.info(f"running block index {{block_idx}}.")
|
|
2088
|
+
|
|
2089
|
+
os.environ["{app_caps}_BLOCK_IDX"] = str(block_idx)
|
|
2090
|
+
|
|
2091
|
+
block_run_IDs = [
|
|
2092
|
+
run_IDs[block_start_elem_idx + i]
|
|
2093
|
+
for i in range(num_elements[block_idx])
|
|
2094
|
+
]
|
|
2095
|
+
|
|
2096
|
+
for block_act_idx in range(num_actions[block_idx]):
|
|
2097
|
+
|
|
2098
|
+
app.logger.info(
|
|
2099
|
+
f"running block action index {{block_act_idx}} "
|
|
2100
|
+
f"(in block {{block_idx}})."
|
|
2101
|
+
)
|
|
2102
|
+
|
|
2103
|
+
os.environ["{app_caps}_BLOCK_ACT_IDX"] = str(block_act_idx)
|
|
2104
|
+
|
|
2105
|
+
block_act_run_IDs = [i[block_act_idx] for i in block_run_IDs]
|
|
2106
|
+
|
|
2107
|
+
block_act_std_path = Path(
|
|
2108
|
+
os.environ["{app_caps}_SUB_STD_DIR"],
|
|
2109
|
+
f"js_{js_idx}_blk_{{block_idx}}_blk_act_{{block_act_idx}}.txt",
|
|
2110
|
+
)
|
|
2111
|
+
with app.redirect_std_to_file(block_act_std_path):
|
|
2112
|
+
# set run starts for all runs of the block/action:
|
|
2113
|
+
block_act_run_dirs = [run_dirs[i] for i in block_act_run_IDs]
|
|
2114
|
+
block_act_runs = [runs[i] for i in block_act_run_IDs]
|
|
2115
|
+
|
|
2116
|
+
block_act_run_IDs_non_skipped = []
|
|
2117
|
+
block_act_run_dirs_non_skipped = []
|
|
2118
|
+
for i, j in zip(block_act_run_IDs, block_act_run_dirs):
|
|
2119
|
+
if not run_skips[i]:
|
|
2120
|
+
block_act_run_IDs_non_skipped.append(i)
|
|
2121
|
+
block_act_run_dirs_non_skipped.append(j)
|
|
2122
|
+
|
|
2123
|
+
if block_act_run_IDs_non_skipped:
|
|
2124
|
+
set_start_multi_tic = time.perf_counter()
|
|
2125
|
+
app.logger.info("setting run starts.")
|
|
2126
|
+
wk.set_multi_run_starts(block_act_run_IDs_non_skipped, block_act_run_dirs_non_skipped, port)
|
|
2127
|
+
app.logger.info("finished setting run starts.")
|
|
2128
|
+
set_start_multi_toc = time.perf_counter()
|
|
2129
|
+
set_start_multi_time = set_start_multi_toc - set_start_multi_tic
|
|
2130
|
+
print(f"{{set_start_multi_time:.4f}}", file=set_start_multi_times_fp, flush=True)
|
|
2131
|
+
|
|
2132
|
+
all_act_outputs = {{}}
|
|
2133
|
+
run_end_dat = defaultdict(list)
|
|
2134
|
+
block_act_key=({js_idx}, block_idx, block_act_idx)
|
|
2135
|
+
|
|
2136
|
+
for block_elem_idx in range(num_elements[block_idx]):
|
|
2137
|
+
|
|
2138
|
+
js_elem_idx = block_start_elem_idx + block_elem_idx
|
|
2139
|
+
run_ID = block_act_run_IDs[block_elem_idx]
|
|
2140
|
+
|
|
2141
|
+
app.logger.info(
|
|
2142
|
+
f"run_ID is {{run_ID}}; block element index: {{block_elem_idx}}; "
|
|
2143
|
+
f"block action index: {{block_act_idx}}; in block {{block_idx}}."
|
|
2144
|
+
)
|
|
2145
|
+
|
|
2146
|
+
if run_ID == -1:
|
|
2147
|
+
continue
|
|
2148
|
+
|
|
2149
|
+
run = runs[run_ID]
|
|
2150
|
+
|
|
2151
|
+
skip = run_skips[run_ID]
|
|
2152
|
+
if skip:
|
|
2153
|
+
app.logger.info(f"run_ID: {{run_ID}}; run is set to skip; skipping.")
|
|
2154
|
+
# set run end
|
|
2155
|
+
run_end_dat[block_act_key].append((run, {skipped_exit_code}, None))
|
|
2156
|
+
continue
|
|
2157
|
+
|
|
2158
|
+
run_tic = time.perf_counter()
|
|
2159
|
+
|
|
2160
|
+
os.environ["{app_caps}_BLOCK_ELEM_IDX"] = str(block_elem_idx)
|
|
2161
|
+
os.environ["{app_caps}_JS_ELEM_IDX"] = str(js_elem_idx)
|
|
2162
|
+
os.environ["{app_caps}_RUN_ID"] = str(run_ID)
|
|
2163
|
+
|
|
2164
|
+
std_path = Path(os.environ["{app_caps}_SUB_STD_DIR"], f"{{run_ID}}.txt")
|
|
2165
|
+
with app.redirect_std_to_file(std_path):
|
|
2166
|
+
|
|
2167
|
+
if {write_app_logs!r}:
|
|
2168
|
+
new_log_path = Path(
|
|
2169
|
+
os.environ["{app_caps}_SUB_LOG_DIR"],
|
|
2170
|
+
f"{run_log_name}",
|
|
2171
|
+
)
|
|
2172
|
+
# TODO: this doesn't work!
|
|
2173
|
+
app.logger.info(
|
|
2174
|
+
f"run_ID: {{run_ID}}; moving log path to {{new_log_path}}"
|
|
2175
|
+
)
|
|
2176
|
+
app.config.log_path = new_log_path
|
|
2177
|
+
|
|
2178
|
+
run_dir = run_dirs[run_ID]
|
|
2179
|
+
|
|
2180
|
+
script_idx = script_indices[block_idx][block_act_idx]
|
|
2181
|
+
req_dir = requires_dir[script_idx]
|
|
2182
|
+
if req_dir:
|
|
2183
|
+
app.logger.info(f"run_ID: {{run_ID}}; changing to run directory: {{run_dir}}")
|
|
2184
|
+
os.chdir(run_dir)
|
|
2185
|
+
|
|
2186
|
+
# retrieve script inputs:
|
|
2187
|
+
app.logger.info(f"run_ID: {{run_ID}}; retrieving script inputs.")
|
|
2188
|
+
get_ins_tic = time.perf_counter()
|
|
2189
|
+
try:
|
|
2190
|
+
with run.raise_on_failure_threshold() as unset_params:
|
|
2191
|
+
app.logger.info(f"run_ID: {{run_ID}}; writing script input files.")
|
|
2192
|
+
run.write_script_data_in_files(block_act_key)
|
|
2193
|
+
|
|
2194
|
+
app.logger.info(f"run_ID: {{run_ID}}; retrieving funcion kwargs.")
|
|
2195
|
+
func_kwargs = run.get_py_script_func_kwargs(
|
|
2196
|
+
raise_on_unset=False,
|
|
2197
|
+
add_script_files=True,
|
|
2198
|
+
blk_act_key=block_act_key,
|
|
2199
|
+
)
|
|
2200
|
+
app.logger.info(
|
|
2201
|
+
f"run_ID: {{run_ID}}; script inputs have keys: "
|
|
2202
|
+
f"{{tuple(func_kwargs.keys())!r}}."
|
|
2203
|
+
)
|
|
2204
|
+
except UnsetParameterDataErrorBase:
|
|
2205
|
+
# not all required parameter data is set, so fail this run:
|
|
2206
|
+
exit_code = 1
|
|
2207
|
+
run_end_dat[block_act_key].append((run, exit_code, None))
|
|
2208
|
+
app.logger.info(
|
|
2209
|
+
f"run_ID: {{run_ID}}; some parameter data is unset, "
|
|
2210
|
+
f"so cannot run; setting exit code to 1."
|
|
2211
|
+
)
|
|
2212
|
+
continue # don't run the function
|
|
2213
|
+
|
|
2214
|
+
get_ins_toc = time.perf_counter()
|
|
2215
|
+
|
|
2216
|
+
func = action_scripts[script_idx]
|
|
2217
|
+
app.logger.info(f"run_ID: {{run_ID}}; function to run is: {{func.__name__}}")
|
|
2218
|
+
|
|
2219
|
+
|
|
2220
|
+
try:
|
|
2221
|
+
func_tic = time.perf_counter()
|
|
2222
|
+
app.logger.info(f"run_ID: {{run_ID}}; invoking function.")
|
|
2223
|
+
{func_invoc_lines}
|
|
2224
|
+
|
|
2225
|
+
except Exception:
|
|
2226
|
+
print(f"Exception caught during execution of script function {{func.__name__}}.")
|
|
2227
|
+
traceback.print_exc()
|
|
2228
|
+
exit_code = 1
|
|
2229
|
+
outputs = {{}}
|
|
2230
|
+
else:
|
|
2231
|
+
app.logger.info(f"run_ID: {{run_ID}}; finished function invocation.")
|
|
2232
|
+
exit_code = 0
|
|
2233
|
+
finally:
|
|
2234
|
+
func_toc = time.perf_counter()
|
|
2235
|
+
|
|
2236
|
+
with app.redirect_std_to_file(std_path):
|
|
2237
|
+
# set run end
|
|
2238
|
+
block_act_key=({js_idx}, block_idx, block_act_idx)
|
|
2239
|
+
run_end_dat[block_act_key].append((run, exit_code, run_dir))
|
|
2240
|
+
|
|
2241
|
+
# store outputs to save at end:
|
|
2242
|
+
app.logger.info(f"run_ID: {{run_ID}}; setting outputs to save.")
|
|
2243
|
+
for name_i, out_i in outputs.items():
|
|
2244
|
+
p_id = run.data_idx[f"outputs.{{name_i}}"]
|
|
2245
|
+
all_act_outputs[p_id] = out_i
|
|
2246
|
+
app.logger.info(f"run_ID: {{run_ID}}; finished setting outputs to save.")
|
|
2247
|
+
|
|
2248
|
+
if req_dir:
|
|
2249
|
+
app.logger.info(f"run_ID: {{run_ID}}; changing directory back")
|
|
2250
|
+
os.chdir(os.environ["{app_caps}_SUB_TMP_DIR"])
|
|
2251
|
+
|
|
2252
|
+
if {write_app_logs!r}:
|
|
2253
|
+
app.logger.info(f"run_ID: {{run_ID}}; moving log path back to " + {sub_log_path!r})
|
|
2254
|
+
app.config.log_path = {sub_log_path}
|
|
2255
|
+
|
|
2256
|
+
run_toc = time.perf_counter()
|
|
2257
|
+
|
|
2258
|
+
get_ins_time = get_ins_toc - get_ins_tic
|
|
2259
|
+
func_time = func_toc - func_tic
|
|
2260
|
+
run_time = run_toc - run_tic
|
|
2261
|
+
|
|
2262
|
+
print(f"{{get_ins_time:.4f}}", file=get_ins_time_fp)
|
|
2263
|
+
print(f"{{func_time:.4f}}", file=func_time_fp)
|
|
2264
|
+
print(f"{{run_time:.4f}}", file=run_time_fp)
|
|
2265
|
+
|
|
2266
|
+
with app.redirect_std_to_file(block_act_std_path):
|
|
2267
|
+
|
|
2268
|
+
if all_act_outputs:
|
|
2269
|
+
# save outputs of all elements of this action
|
|
2270
|
+
save_all_tic = time.perf_counter()
|
|
2271
|
+
app.logger.info(
|
|
2272
|
+
f"saving outputs of block action index {{block_act_idx}} "
|
|
2273
|
+
f"in block {{block_idx}}."
|
|
2274
|
+
)
|
|
2275
|
+
wk.set_parameter_values(all_act_outputs)
|
|
2276
|
+
app.logger.info(
|
|
2277
|
+
f"finished saving outputs of block action index {{block_act_idx}} "
|
|
2278
|
+
f"in block {{block_idx}}."
|
|
2279
|
+
)
|
|
2280
|
+
save_all_toc = time.perf_counter()
|
|
2281
|
+
save_all_time_i = save_all_toc - save_all_tic
|
|
2282
|
+
print(f"{{save_all_time_i:.4f}}", file=save_multi_times_fp, flush=True)
|
|
2283
|
+
|
|
2284
|
+
all_loop_term_tic = time.perf_counter()
|
|
2285
|
+
app.logger.info(f"run_ID: {{run_ID}}; checking for loop terminations")
|
|
2286
|
+
for run_i in block_act_runs:
|
|
2287
|
+
if not run_skips[run_i.id_]:
|
|
2288
|
+
skipped_IDs_i = wk._check_loop_termination(run_i)
|
|
2289
|
+
for skip_ID in skipped_IDs_i:
|
|
2290
|
+
run_skips[skip_ID] = 2 # SkipReason.LOOP_TERMINATION
|
|
2291
|
+
if skip_ID in runs:
|
|
2292
|
+
runs[skip_ID]._skip = 2 # mutates runs within `run_end_dat`
|
|
2293
|
+
app.logger.info(f"run_ID: {{run_ID}}; finished checking for loop terminations.")
|
|
2294
|
+
|
|
2295
|
+
all_loop_term_toc = time.perf_counter()
|
|
2296
|
+
all_loop_term_time_i = all_loop_term_toc - all_loop_term_tic
|
|
2297
|
+
print(f"{{all_loop_term_time_i:.4f}}", file=loop_term_times_fp, flush=True)
|
|
2298
|
+
|
|
2299
|
+
# set run end for all elements of this action
|
|
2300
|
+
app.logger.info(f"run_ID: {{run_ID}}; setting run ends.")
|
|
2301
|
+
set_multi_end_tic = time.perf_counter()
|
|
2302
|
+
wk.set_multi_run_ends(run_end_dat)
|
|
2303
|
+
set_multi_end_toc = time.perf_counter()
|
|
2304
|
+
set_multi_end_time = set_multi_end_toc - set_multi_end_tic
|
|
2305
|
+
app.logger.info(f"run_ID: {{run_ID}}; finished setting run ends.")
|
|
2306
|
+
print(f"{{set_multi_end_time:.4f}}", file=set_end_multi_times_fp, flush=True)
|
|
2307
|
+
|
|
2308
|
+
block_start_elem_idx += num_elements[block_idx]
|
|
2309
|
+
|
|
2310
|
+
get_ins_time_fp.close()
|
|
2311
|
+
func_time_fp.close()
|
|
2312
|
+
run_time_fp.close()
|
|
2313
|
+
set_start_multi_times_fp.close()
|
|
2314
|
+
set_end_multi_times_fp.close()
|
|
2315
|
+
save_multi_times_fp.close()
|
|
2316
|
+
loop_term_times_fp.close()
|
|
2317
|
+
"""
|
|
2318
|
+
).format(
|
|
2319
|
+
py_imports=py_imports,
|
|
2320
|
+
py_main_block_workflow_load=indent(py_main_block_workflow_load, tab_indent),
|
|
2321
|
+
app_caps=self._app.package_name.upper(),
|
|
2322
|
+
script_idx_delim=",", # TODO
|
|
2323
|
+
script_names=script_names_str,
|
|
2324
|
+
requires_dir=requires_dir,
|
|
2325
|
+
num_blocks=len(self.blocks),
|
|
2326
|
+
run_ID_delim=self._EAR_files_delimiter,
|
|
2327
|
+
run_log_name=self.submission.get_app_log_file_name(run_ID="{run_ID}"),
|
|
2328
|
+
js_idx=self.index,
|
|
2329
|
+
write_app_logs=self.resources.write_app_logs,
|
|
2330
|
+
sub_log_path=sub_log_path,
|
|
2331
|
+
skipped_exit_code=SKIPPED_EXIT_CODE,
|
|
2332
|
+
func_invoc_lines=indent(func_invoc_lines, tab_indent * 4),
|
|
2333
|
+
)
|
|
2334
|
+
|
|
2335
|
+
future_imports_str = (
|
|
2336
|
+
f"from __future__ import {', '.join(future_imports)}\n\n"
|
|
2337
|
+
if future_imports
|
|
2338
|
+
else ""
|
|
2339
|
+
)
|
|
2340
|
+
script = dedent(
|
|
2341
|
+
"""\
|
|
2342
|
+
{future_imports_str}{script_funcs}
|
|
2343
|
+
if __name__ == "__main__":
|
|
2344
|
+
{main}
|
|
2345
|
+
"""
|
|
2346
|
+
).format(
|
|
2347
|
+
future_imports_str=future_imports_str,
|
|
2348
|
+
script_funcs=script_funcs,
|
|
2349
|
+
main=indent(main, tab_indent),
|
|
2350
|
+
)
|
|
2351
|
+
|
|
2352
|
+
num_elems = [i.num_elements for i in self.blocks]
|
|
2353
|
+
num_acts = [len(i) for i in action_scripts]
|
|
2354
|
+
|
|
2355
|
+
return script, script_indices, num_elems, num_acts
|
|
2356
|
+
|
|
2357
|
+
def write_script_indices_file(
|
|
2358
|
+
self, indices: list[list[int]], num_elems: list[int], num_acts: list[int]
|
|
2359
|
+
) -> None:
|
|
2360
|
+
"""
|
|
2361
|
+
Write a text file containing the action script index for each block and action
|
|
2362
|
+
in a `combined_scripts` script.
|
|
2363
|
+
"""
|
|
2364
|
+
delim = "," # TODO: refactor?
|
|
2365
|
+
with self.combined_script_indices_file_path.open("wt") as fp:
|
|
2366
|
+
fp.write("# number of elements per block:\n")
|
|
2367
|
+
fp.write(delim.join(str(i) for i in num_elems) + "\n")
|
|
2368
|
+
fp.write("# number of actions per block:\n")
|
|
2369
|
+
fp.write(delim.join(str(i) for i in num_acts) + "\n")
|
|
2370
|
+
fp.write("# script indices:\n")
|
|
2371
|
+
for block in indices:
|
|
2372
|
+
fp.write(delim.join(str(i) for i in block) + "\n")
|
|
2373
|
+
|
|
2374
|
+
def get_app_std_path(self) -> Path:
|
|
2375
|
+
std_dir = self.submission.get_app_std_path(
|
|
2376
|
+
self.workflow.submissions_path,
|
|
2377
|
+
self.submission.index,
|
|
2378
|
+
)
|
|
2379
|
+
return std_dir / f"js_{self.index}.txt" # TODO: refactor
|