hpcflow-new2 0.2.0a50__py3-none-any.whl → 0.2.0a52__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/_version.py +1 -1
- hpcflow/sdk/__init__.py +1 -1
- hpcflow/sdk/api.py +1 -1
- hpcflow/sdk/app.py +20 -11
- hpcflow/sdk/cli.py +34 -59
- hpcflow/sdk/core/__init__.py +13 -1
- hpcflow/sdk/core/actions.py +235 -126
- hpcflow/sdk/core/command_files.py +32 -24
- hpcflow/sdk/core/element.py +110 -114
- hpcflow/sdk/core/errors.py +57 -0
- hpcflow/sdk/core/loop.py +18 -34
- hpcflow/sdk/core/parameters.py +5 -3
- hpcflow/sdk/core/task.py +135 -131
- hpcflow/sdk/core/task_schema.py +11 -4
- hpcflow/sdk/core/utils.py +110 -2
- hpcflow/sdk/core/workflow.py +964 -676
- hpcflow/sdk/data/template_components/environments.yaml +0 -44
- hpcflow/sdk/data/template_components/task_schemas.yaml +52 -10
- hpcflow/sdk/persistence/__init__.py +21 -33
- hpcflow/sdk/persistence/base.py +1340 -458
- hpcflow/sdk/persistence/json.py +424 -546
- hpcflow/sdk/persistence/pending.py +563 -0
- hpcflow/sdk/persistence/store_resource.py +131 -0
- hpcflow/sdk/persistence/utils.py +57 -0
- hpcflow/sdk/persistence/zarr.py +852 -841
- hpcflow/sdk/submission/jobscript.py +133 -112
- hpcflow/sdk/submission/shells/bash.py +62 -16
- hpcflow/sdk/submission/shells/powershell.py +87 -16
- hpcflow/sdk/submission/submission.py +59 -35
- hpcflow/tests/unit/test_element.py +4 -9
- hpcflow/tests/unit/test_persistence.py +218 -0
- hpcflow/tests/unit/test_task.py +11 -12
- hpcflow/tests/unit/test_utils.py +82 -0
- hpcflow/tests/unit/test_workflow.py +3 -1
- {hpcflow_new2-0.2.0a50.dist-info → hpcflow_new2-0.2.0a52.dist-info}/METADATA +3 -1
- {hpcflow_new2-0.2.0a50.dist-info → hpcflow_new2-0.2.0a52.dist-info}/RECORD +38 -34
- {hpcflow_new2-0.2.0a50.dist-info → hpcflow_new2-0.2.0a52.dist-info}/WHEEL +0 -0
- {hpcflow_new2-0.2.0a50.dist-info → hpcflow_new2-0.2.0a52.dist-info}/entry_points.txt +0 -0
@@ -4,6 +4,7 @@ import copy
|
|
4
4
|
from datetime import datetime
|
5
5
|
import os
|
6
6
|
from pathlib import Path
|
7
|
+
import shutil
|
7
8
|
import subprocess
|
8
9
|
from textwrap import indent
|
9
10
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
@@ -11,7 +12,6 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
12
|
import numpy as np
|
12
13
|
from numpy.typing import NDArray
|
13
14
|
from hpcflow.sdk import app
|
14
|
-
from hpcflow.sdk.core.actions import ElementID
|
15
15
|
from hpcflow.sdk.core.errors import JobscriptSubmissionFailure
|
16
16
|
|
17
17
|
from hpcflow.sdk.core.json_like import ChildObjectSpec, JSONLike
|
@@ -45,14 +45,16 @@ def generate_EAR_resource_map(
|
|
45
45
|
|
46
46
|
arr_shape = (task.num_actions, task.num_elements)
|
47
47
|
resource_map = np.empty(arr_shape, dtype=int)
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
)
|
48
|
+
EAR_ID_map = np.empty(arr_shape, dtype=int)
|
49
|
+
# EAR_idx_map = np.empty(
|
50
|
+
# shape=arr_shape,
|
51
|
+
# dtype=[("EAR_idx", np.int32), ("run_idx", np.int32), ("iteration_idx", np.int32)],
|
52
|
+
# )
|
52
53
|
resource_map[:] = none_val
|
53
|
-
|
54
|
+
EAR_ID_map[:] = none_val
|
55
|
+
# EAR_idx_map[:] = (none_val, none_val, none_val) # TODO: add iteration_idx as well
|
54
56
|
|
55
|
-
for element in task.elements:
|
57
|
+
for element in task.elements[:]:
|
56
58
|
for iter_i in element.iterations:
|
57
59
|
if iter_i.loop_idx != loop_idx:
|
58
60
|
continue
|
@@ -68,17 +70,18 @@ def generate_EAR_resource_map(
|
|
68
70
|
resource_map[act_idx][element.index] = resource_hashes.index(
|
69
71
|
res_hash
|
70
72
|
)
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
73
|
+
EAR_ID_map[act_idx, element.index] = run.id_
|
74
|
+
# EAR_idx_map[act_idx, element.index] = (
|
75
|
+
# run.index,
|
76
|
+
# run.run_idx,
|
77
|
+
# iter_i.index,
|
78
|
+
# )
|
76
79
|
|
77
80
|
return (
|
78
81
|
resources,
|
79
82
|
resource_hashes,
|
80
83
|
resource_map,
|
81
|
-
|
84
|
+
EAR_ID_map,
|
82
85
|
)
|
83
86
|
|
84
87
|
|
@@ -162,13 +165,13 @@ def resolve_jobscript_dependencies(jobscripts, element_deps):
|
|
162
165
|
jobscript_deps[js_idx] = {}
|
163
166
|
|
164
167
|
for js_elem_idx_i, EAR_deps_i in elem_deps.items():
|
168
|
+
# locate which jobscript elements this jobscript element depends on:
|
165
169
|
for EAR_dep_j in EAR_deps_i:
|
166
|
-
# locate which jobscript(s) this element depends on:
|
167
170
|
for js_k_idx, js_k in jobscripts.items():
|
168
171
|
if js_k_idx == js_idx:
|
169
172
|
break
|
170
173
|
|
171
|
-
if EAR_dep_j in js_k["
|
174
|
+
if EAR_dep_j in js_k["EAR_ID"]:
|
172
175
|
if js_k_idx not in jobscript_deps[js_idx]:
|
173
176
|
jobscript_deps[js_idx][js_k_idx] = {"js_element_mapping": {}}
|
174
177
|
|
@@ -180,14 +183,21 @@ def resolve_jobscript_dependencies(jobscripts, element_deps):
|
|
180
183
|
js_elem_idx_i
|
181
184
|
] = []
|
182
185
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
186
|
+
# retrieve column index, which is the JS-element index:
|
187
|
+
js_elem_idx_k = np.where(
|
188
|
+
np.any(js_k["EAR_ID"] == EAR_dep_j, axis=0)
|
189
|
+
)[0][0].item()
|
190
|
+
|
191
|
+
# add js dependency element-mapping:
|
192
|
+
if (
|
193
|
+
js_elem_idx_k
|
194
|
+
not in jobscript_deps[js_idx][js_k_idx]["js_element_mapping"][
|
195
|
+
js_elem_idx_i
|
196
|
+
]
|
197
|
+
):
|
198
|
+
jobscript_deps[js_idx][js_k_idx]["js_element_mapping"][
|
199
|
+
js_elem_idx_i
|
200
|
+
].append(js_elem_idx_k)
|
191
201
|
|
192
202
|
# next we can determine if two jobscripts have a one-to-one element mapping, which
|
193
203
|
# means they can be submitted with a "job array" dependency relationship:
|
@@ -195,8 +205,8 @@ def resolve_jobscript_dependencies(jobscripts, element_deps):
|
|
195
205
|
for js_k_idx, deps_j in deps_i.items():
|
196
206
|
# is this an array dependency?
|
197
207
|
|
198
|
-
js_i_num_js_elements = jobscripts[js_i_idx]["
|
199
|
-
js_k_num_js_elements = jobscripts[js_k_idx]["
|
208
|
+
js_i_num_js_elements = jobscripts[js_i_idx]["EAR_ID"].shape[1]
|
209
|
+
js_k_num_js_elements = jobscripts[js_k_idx]["EAR_ID"].shape[1]
|
200
210
|
|
201
211
|
is_all_i_elems = list(
|
202
212
|
sorted(set(deps_j["js_element_mapping"].keys()))
|
@@ -207,7 +217,7 @@ def resolve_jobscript_dependencies(jobscripts, element_deps):
|
|
207
217
|
) == {1}
|
208
218
|
|
209
219
|
is_all_k_elems = list(
|
210
|
-
sorted(
|
220
|
+
sorted(i[0] for i in deps_j["js_element_mapping"].values())
|
211
221
|
) == list(range(js_k_num_js_elements))
|
212
222
|
|
213
223
|
is_arr = is_all_i_elems and is_all_k_single and is_all_k_elems
|
@@ -233,7 +243,9 @@ def merge_jobscripts_across_tasks(jobscripts: Dict) -> Dict:
|
|
233
243
|
|
234
244
|
# can only merge if resources are the same and is array dependency:
|
235
245
|
if js["resource_hash"] == js_j["resource_hash"] and dep_info["is_array"]:
|
236
|
-
num_loop_idx = len(
|
246
|
+
num_loop_idx = len(
|
247
|
+
js_j["task_loop_idx"]
|
248
|
+
) # TODO: should this be: `js_j["task_loop_idx"][0]`?
|
237
249
|
|
238
250
|
# append task_insert_IDs
|
239
251
|
js_j["task_insert_IDs"].append(js["task_insert_IDs"][0])
|
@@ -249,10 +261,10 @@ def merge_jobscripts_across_tasks(jobscripts: Dict) -> Dict:
|
|
249
261
|
js_j["task_elements"].update(js["task_elements"])
|
250
262
|
|
251
263
|
# update EARs dict
|
252
|
-
js_j["EARs"].update(js["EARs"])
|
264
|
+
# js_j["EARs"].update(js["EARs"])
|
253
265
|
|
254
266
|
# append to elements and elements_idx list
|
255
|
-
js_j["
|
267
|
+
js_j["EAR_ID"] = np.vstack((js_j["EAR_ID"], js["EAR_ID"]))
|
256
268
|
|
257
269
|
# mark this js as defunct
|
258
270
|
js["is_merged"] = True
|
@@ -311,8 +323,8 @@ class Jobscript(JSONLike):
|
|
311
323
|
task_insert_IDs: List[int],
|
312
324
|
task_actions: List[Tuple],
|
313
325
|
task_elements: Dict[int, List[int]],
|
314
|
-
EARs: Dict[Tuple[int] : Tuple[int]],
|
315
|
-
|
326
|
+
# EARs: Dict[Tuple[int] : Tuple[int]],
|
327
|
+
EAR_ID: NDArray,
|
316
328
|
resources: app.ElementResources,
|
317
329
|
task_loop_idx: List[Dict],
|
318
330
|
dependencies: Dict[int:Dict],
|
@@ -324,8 +336,8 @@ class Jobscript(JSONLike):
|
|
324
336
|
self._task_loop_idx = task_loop_idx
|
325
337
|
self._task_actions = task_actions
|
326
338
|
self._task_elements = task_elements
|
327
|
-
self._EARs = EARs
|
328
|
-
self.
|
339
|
+
# self._EARs = EARs
|
340
|
+
self._EAR_ID = EAR_ID
|
329
341
|
self._resources = resources
|
330
342
|
self._dependencies = dependencies
|
331
343
|
|
@@ -355,20 +367,22 @@ class Jobscript(JSONLike):
|
|
355
367
|
del dct["_scheduler_obj"]
|
356
368
|
del dct["_shell_obj"]
|
357
369
|
dct = {k.lstrip("_"): v for k, v in dct.items()}
|
358
|
-
dct["
|
359
|
-
dct["EARs"] = [[list(k), list(v)] for k, v in dct["EARs"].items()]
|
360
|
-
|
361
|
-
|
370
|
+
dct["EAR_ID"] = dct["EAR_ID"].tolist()
|
371
|
+
# dct["EARs"] = [[list(k), list(v)] for k, v in dct["EARs"].items()]
|
372
|
+
# TODO: this needed?
|
373
|
+
# if dct.get("scheduler_version_info"):
|
374
|
+
# dct["scheduler_version_info"] = list(dct["scheduler_version_info"])
|
362
375
|
return dct
|
363
376
|
|
364
377
|
@classmethod
|
365
378
|
def from_json_like(cls, json_like, shared_data=None):
|
366
|
-
json_like["
|
367
|
-
json_like["EARs"] = {tuple(i[0]): tuple(i[1]) for i in json_like["EARs"]}
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
379
|
+
json_like["EAR_ID"] = np.array(json_like["EAR_ID"])
|
380
|
+
# json_like["EARs"] = {tuple(i[0]): tuple(i[1]) for i in json_like["EARs"]}
|
381
|
+
# TODO: this needed?
|
382
|
+
# if json_like.get("scheduler_version_info"):
|
383
|
+
# json_like["scheduler_version_info"] = tuple(
|
384
|
+
# json_like["scheduler_version_info"]
|
385
|
+
# )
|
372
386
|
return super().from_json_like(json_like, shared_data)
|
373
387
|
|
374
388
|
@property
|
@@ -390,13 +404,13 @@ class Jobscript(JSONLike):
|
|
390
404
|
def task_elements(self):
|
391
405
|
return self._task_elements
|
392
406
|
|
393
|
-
@property
|
394
|
-
def EARs(self):
|
395
|
-
|
407
|
+
# @property
|
408
|
+
# def EARs(self):
|
409
|
+
# return self._EARs
|
396
410
|
|
397
411
|
@property
|
398
|
-
def
|
399
|
-
return self.
|
412
|
+
def EAR_ID(self):
|
413
|
+
return self._EAR_ID
|
400
414
|
|
401
415
|
@property
|
402
416
|
def resources(self):
|
@@ -419,7 +433,7 @@ class Jobscript(JSONLike):
|
|
419
433
|
return self._scheduler_job_ID
|
420
434
|
|
421
435
|
@property
|
422
|
-
def
|
436
|
+
def version_info(self):
|
423
437
|
return self._version_info
|
424
438
|
|
425
439
|
@property
|
@@ -436,18 +450,18 @@ class Jobscript(JSONLike):
|
|
436
450
|
|
437
451
|
@property
|
438
452
|
def num_actions(self):
|
439
|
-
return self.
|
453
|
+
return self.EAR_ID.shape[0]
|
440
454
|
|
441
455
|
@property
|
442
456
|
def num_elements(self):
|
443
|
-
return self.
|
457
|
+
return self.EAR_ID.shape[1]
|
444
458
|
|
445
459
|
@property
|
446
460
|
def is_array(self):
|
447
461
|
if not self.scheduler_name:
|
448
462
|
return False
|
449
463
|
|
450
|
-
support_EAR_para = self.workflow._store.
|
464
|
+
support_EAR_para = self.workflow._store._features.EAR_parallelism
|
451
465
|
if self.resources.use_job_array is None:
|
452
466
|
if self.num_elements > 1 and support_EAR_para:
|
453
467
|
return True
|
@@ -502,8 +516,8 @@ class Jobscript(JSONLike):
|
|
502
516
|
return self._scheduler_obj
|
503
517
|
|
504
518
|
@property
|
505
|
-
def
|
506
|
-
return f"js_{self.index}
|
519
|
+
def EAR_ID_file_name(self):
|
520
|
+
return f"js_{self.index}_EAR_IDs.txt"
|
507
521
|
|
508
522
|
@property
|
509
523
|
def element_run_dir_file_name(self):
|
@@ -514,8 +528,8 @@ class Jobscript(JSONLike):
|
|
514
528
|
return f"js_{self.index}{self.shell.JS_EXT}"
|
515
529
|
|
516
530
|
@property
|
517
|
-
def
|
518
|
-
return self.submission.path / self.
|
531
|
+
def EAR_ID_file_path(self):
|
532
|
+
return self.submission.path / self.EAR_ID_file_name
|
519
533
|
|
520
534
|
@property
|
521
535
|
def element_run_dir_file_path(self):
|
@@ -549,49 +563,51 @@ class Jobscript(JSONLike):
|
|
549
563
|
vers_info=vers_info,
|
550
564
|
)
|
551
565
|
|
552
|
-
def get_task_insert_IDs_array(self):
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
566
|
+
# def get_task_insert_IDs_array(self):
|
567
|
+
# # TODO: probably won't need this.
|
568
|
+
# task_insert_IDs = np.empty_like(self.EAR_ID)
|
569
|
+
# task_insert_IDs[:] = np.array([i[0] for i in self.task_actions]).reshape(
|
570
|
+
# (len(self.task_actions), 1)
|
571
|
+
# )
|
572
|
+
# return task_insert_IDs
|
558
573
|
|
559
574
|
def get_task_loop_idx_array(self):
|
560
|
-
loop_idx = np.empty_like(self.
|
575
|
+
loop_idx = np.empty_like(self.EAR_ID)
|
561
576
|
loop_idx[:] = np.array([i[2] for i in self.task_actions]).reshape(
|
562
577
|
(len(self.task_actions), 1)
|
563
578
|
)
|
564
579
|
return loop_idx
|
565
580
|
|
566
|
-
def get_task_element_idx_array(self):
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
581
|
+
# def get_task_element_idx_array(self):
|
582
|
+
# # TODO: probably won't need this.
|
583
|
+
# element_idx = np.empty_like(self.EAR_ID)
|
584
|
+
# for task_iID, elem_idx in self.task_elements.items():
|
585
|
+
# rows_idx = [
|
586
|
+
# idx for idx, i in enumerate(self.task_actions) if i[0] == task_iID
|
587
|
+
# ]
|
588
|
+
# element_idx[rows_idx] = elem_idx
|
589
|
+
# return element_idx
|
590
|
+
|
591
|
+
# def get_EAR_run_idx_array(self):
|
592
|
+
# # TODO: probably won't need this.
|
593
|
+
# task_insert_ID_arr = self.get_task_insert_IDs_array()
|
594
|
+
# element_idx = self.get_task_element_idx_array()
|
595
|
+
# run_idx = np.empty_like(self.EAR_ID)
|
596
|
+
# for js_act_idx in range(self.num_actions):
|
597
|
+
# for js_elem_idx in range(self.num_elements):
|
598
|
+
# EAR_idx_i = self.EAR_ID[js_act_idx, js_elem_idx]
|
599
|
+
# task_iID_i = task_insert_ID_arr[js_act_idx, js_elem_idx]
|
600
|
+
# elem_idx_i = element_idx[js_act_idx, js_elem_idx]
|
601
|
+
# (_, _, run_idx_i) = self.EARs[(task_iID_i, elem_idx_i, EAR_idx_i)]
|
602
|
+
# run_idx[js_act_idx, js_elem_idx] = run_idx_i
|
603
|
+
# return run_idx
|
589
604
|
|
590
605
|
def get_EAR_ID_array(self):
|
606
|
+
# TODO: probably won't need this.
|
591
607
|
task_insert_ID_arr = self.get_task_insert_IDs_array()
|
592
608
|
element_idx = self.get_task_element_idx_array()
|
593
609
|
EAR_ID_arr = np.empty(
|
594
|
-
shape=self.
|
610
|
+
shape=self.EAR_ID.shape,
|
595
611
|
dtype=[
|
596
612
|
("task_insert_ID", np.int32),
|
597
613
|
("element_idx", np.int32),
|
@@ -604,7 +620,7 @@ class Jobscript(JSONLike):
|
|
604
620
|
|
605
621
|
for js_act_idx in range(self.num_actions):
|
606
622
|
for js_elem_idx in range(self.num_elements):
|
607
|
-
EAR_idx_i = self.
|
623
|
+
EAR_idx_i = self.EAR_ID[js_act_idx, js_elem_idx]
|
608
624
|
task_iID_i = task_insert_ID_arr[js_act_idx, js_elem_idx]
|
609
625
|
elem_idx_i = element_idx[js_act_idx, js_elem_idx]
|
610
626
|
(iter_idx_i, act_idx_i, run_idx_i) = self.EARs[
|
@@ -621,15 +637,15 @@ class Jobscript(JSONLike):
|
|
621
637
|
|
622
638
|
return EAR_ID_arr
|
623
639
|
|
624
|
-
def
|
640
|
+
def write_EAR_ID_file(self):
|
625
641
|
"""Write a text file with `num_elements` lines and `num_actions` delimited tokens
|
626
642
|
per line, representing whether a given EAR must be executed."""
|
627
643
|
|
628
|
-
with self.
|
644
|
+
with self.EAR_ID_file_path.open(mode="wt", newline="\n") as fp:
|
629
645
|
# can't specify "open" newline if we pass the file name only, so pass handle:
|
630
646
|
np.savetxt(
|
631
647
|
fname=fp,
|
632
|
-
X=(self.
|
648
|
+
X=(self.EAR_ID).T,
|
633
649
|
fmt="%.0f",
|
634
650
|
delimiter=self._EAR_files_delimiter,
|
635
651
|
)
|
@@ -650,7 +666,7 @@ class Jobscript(JSONLike):
|
|
650
666
|
fname=fp,
|
651
667
|
X=np.array(run_dirs),
|
652
668
|
fmt="%s",
|
653
|
-
delimiter=
|
669
|
+
delimiter=self._EAR_files_delimiter,
|
654
670
|
)
|
655
671
|
|
656
672
|
def compose_jobscript(self) -> str:
|
@@ -669,12 +685,13 @@ class Jobscript(JSONLike):
|
|
669
685
|
"workflow_app_alias": self.workflow_app_alias,
|
670
686
|
"env_setup": env_setup,
|
671
687
|
"app_invoc": app_invoc,
|
688
|
+
"app_package_name": self.app.package_name,
|
672
689
|
"config_dir": str(self.app.config.config_directory),
|
673
690
|
"config_invoc_key": self.app.config.config_invocation_key,
|
674
691
|
"workflow_path": self.workflow.path,
|
675
692
|
"sub_idx": self.submission.index,
|
676
693
|
"js_idx": self.index,
|
677
|
-
"EAR_file_name": self.
|
694
|
+
"EAR_file_name": self.EAR_ID_file_name,
|
678
695
|
"element_run_dirs_file_path": self.element_run_dir_file_name,
|
679
696
|
}
|
680
697
|
)
|
@@ -734,34 +751,39 @@ class Jobscript(JSONLike):
|
|
734
751
|
fp.write(js_str)
|
735
752
|
return self.jobscript_path
|
736
753
|
|
737
|
-
def
|
738
|
-
|
754
|
+
def _get_EARs_arr(self):
|
755
|
+
EARs_flat = self.workflow.get_EARs_from_IDs(self.EAR_ID.flatten())
|
756
|
+
EARs_arr = np.array(EARs_flat).reshape(self.EAR_ID.shape)
|
757
|
+
return EARs_arr
|
758
|
+
|
759
|
+
def make_artifact_dirs(self):
|
760
|
+
EARs_arr = self._get_EARs_arr()
|
739
761
|
task_loop_idx_arr = self.get_task_loop_idx_array()
|
740
|
-
element_idx = self.get_task_element_idx_array()
|
741
|
-
run_idx = self.get_EAR_run_idx_array()
|
742
762
|
|
743
763
|
run_dirs = []
|
744
764
|
for js_elem_idx in range(self.num_elements):
|
745
765
|
run_dirs_i = []
|
746
766
|
for js_act_idx in range(self.num_actions):
|
747
|
-
|
767
|
+
EAR_i = EARs_arr[js_act_idx, js_elem_idx]
|
768
|
+
t_iID = EAR_i.task.insert_ID
|
748
769
|
l_idx = task_loop_idx_arr[js_act_idx, js_elem_idx].item()
|
749
|
-
|
750
|
-
r_idx = run_idx[js_act_idx, js_elem_idx].item()
|
751
|
-
|
752
|
-
loop_idx = self.task_loop_idx[l_idx]
|
753
|
-
task_dir = self.workflow.tasks.get(insert_ID=t_iID).get_dir_name(loop_idx)
|
754
|
-
|
755
|
-
# TODO: don't load element, but make sure format is the same
|
756
|
-
element_ID = ElementID(task_insert_ID=t_iID, element_idx=e_idx)
|
757
|
-
element = self.workflow.get_elements_from_IDs([element_ID])[0]
|
758
|
-
elem_dir = element.dir_name
|
770
|
+
r_idx = EAR_i.index
|
759
771
|
|
772
|
+
loop_idx_i = self.task_loop_idx[l_idx]
|
773
|
+
task_dir = self.workflow.tasks.get(insert_ID=t_iID).get_dir_name(
|
774
|
+
loop_idx_i
|
775
|
+
)
|
776
|
+
elem_dir = EAR_i.element.dir_name
|
760
777
|
run_dir = f"r_{r_idx}"
|
761
778
|
|
762
|
-
EAR_dir = Path(
|
779
|
+
EAR_dir = Path(self.workflow.execution_path, task_dir, elem_dir, run_dir)
|
763
780
|
EAR_dir.mkdir(exist_ok=True, parents=True)
|
764
781
|
|
782
|
+
# copy (TODO: optionally symlink) any input files:
|
783
|
+
for name, path in EAR_i.get("input_files", {}).items():
|
784
|
+
if path:
|
785
|
+
shutil.copy(path, EAR_dir)
|
786
|
+
|
765
787
|
run_dirs_i.append(EAR_dir.relative_to(self.workflow.path))
|
766
788
|
|
767
789
|
run_dirs.append(run_dirs_i)
|
@@ -770,12 +792,11 @@ class Jobscript(JSONLike):
|
|
770
792
|
|
771
793
|
def submit(
|
772
794
|
self,
|
773
|
-
task_artifacts_path: Path,
|
774
795
|
scheduler_refs: Dict[int, str],
|
775
796
|
print_stdout: Optional[bool] = False,
|
776
797
|
) -> str:
|
777
|
-
run_dirs = self.make_artifact_dirs(
|
778
|
-
self.
|
798
|
+
run_dirs = self.make_artifact_dirs()
|
799
|
+
self.write_EAR_ID_file()
|
779
800
|
self.write_element_run_dir_file(run_dirs)
|
780
801
|
js_path = self.write_jobscript()
|
781
802
|
js_path = self.shell.prepare_JS_path(js_path)
|
@@ -2,6 +2,7 @@ from pathlib import Path
|
|
2
2
|
import subprocess
|
3
3
|
from textwrap import dedent, indent
|
4
4
|
from typing import Dict, List, Optional, Union
|
5
|
+
from hpcflow.sdk.core import ABORT_EXIT_CODE
|
5
6
|
from hpcflow.sdk.submission.shells import Shell
|
6
7
|
from hpcflow.sdk.submission.shells.os_version import (
|
7
8
|
get_OS_info_POSIX,
|
@@ -23,6 +24,7 @@ class Bash(Shell):
|
|
23
24
|
{workflow_app_alias} () {{
|
24
25
|
(
|
25
26
|
{env_setup}{app_invoc}\\
|
27
|
+
--with-config log_file_path "`pwd`/{app_package_name}.log"\\
|
26
28
|
--config-dir "{config_dir}"\\
|
27
29
|
--config-invocation-key "{config_invoc_key}"\\
|
28
30
|
"$@"
|
@@ -54,24 +56,33 @@ class Bash(Shell):
|
|
54
56
|
)
|
55
57
|
JS_MAIN = dedent(
|
56
58
|
"""\
|
57
|
-
|
59
|
+
elem_EAR_IDs=`sed "$((${{JS_elem_idx}} + 1))q;d" $EAR_ID_FILE`
|
58
60
|
elem_run_dirs=`sed "$((${{JS_elem_idx}} + 1))q;d" $ELEM_RUN_DIR_FILE`
|
59
61
|
|
60
62
|
for ((JS_act_idx=0;JS_act_idx<{num_actions};JS_act_idx++))
|
61
63
|
do
|
62
64
|
|
63
|
-
|
64
|
-
if [ "$
|
65
|
+
EAR_ID="$(cut -d'{EAR_files_delimiter}' -f $(($JS_act_idx + 1)) <<< $elem_EAR_IDs)"
|
66
|
+
if [ "$EAR_ID" = "-1" ]; then
|
65
67
|
continue
|
66
68
|
fi
|
67
69
|
|
68
70
|
run_dir="$(cut -d'{EAR_files_delimiter}' -f $(($JS_act_idx + 1)) <<< $elem_run_dirs)"
|
69
71
|
cd $WK_PATH/$run_dir
|
72
|
+
|
73
|
+
skip=`{workflow_app_alias} internal workflow $WK_PATH_ARG get-ear-skipped $EAR_ID`
|
74
|
+
if [ "$skip" = "1" ]; then
|
75
|
+
continue
|
76
|
+
fi
|
70
77
|
|
71
|
-
{workflow_app_alias} internal workflow $WK_PATH_ARG write-commands $SUB_IDX $JS_IDX $
|
72
|
-
{workflow_app_alias} internal workflow $WK_PATH_ARG set-ear-start $
|
78
|
+
{workflow_app_alias} internal workflow $WK_PATH_ARG write-commands $SUB_IDX $JS_IDX $JS_act_idx $EAR_ID
|
79
|
+
{workflow_app_alias} internal workflow $WK_PATH_ARG set-ear-start $EAR_ID
|
80
|
+
|
73
81
|
. {commands_file_name}
|
74
|
-
|
82
|
+
|
83
|
+
exit_code=$?
|
84
|
+
|
85
|
+
{workflow_app_alias} internal workflow $WK_PATH_ARG set-ear-end $EAR_ID $exit_code
|
75
86
|
|
76
87
|
done
|
77
88
|
"""
|
@@ -138,36 +149,71 @@ class Bash(Shell):
|
|
138
149
|
def format_stream_assignment(self, shell_var_name, command):
|
139
150
|
return f"{shell_var_name}=`{command}`"
|
140
151
|
|
141
|
-
def format_save_parameter(
|
152
|
+
def format_save_parameter(
|
153
|
+
self, workflow_app_alias, param_name, shell_var_name, EAR_ID
|
154
|
+
):
|
142
155
|
return (
|
143
156
|
f"{workflow_app_alias}"
|
144
157
|
f" internal workflow $WK_PATH_ARG save-parameter {param_name} ${shell_var_name}"
|
145
|
-
f"
|
158
|
+
f" {EAR_ID}"
|
146
159
|
f"\n"
|
147
160
|
)
|
148
161
|
|
149
|
-
def wrap_in_subshell(self, commands: str) -> str:
|
162
|
+
def wrap_in_subshell(self, commands: str, abortable: bool) -> str:
|
150
163
|
"""Format commands to run within a subshell.
|
151
164
|
|
152
165
|
This assumes commands ends in a newline.
|
153
166
|
|
154
167
|
"""
|
155
168
|
commands = indent(commands, self.JS_INDENT)
|
156
|
-
|
157
|
-
|
158
|
-
(
|
159
|
-
|
160
|
-
|
161
|
-
|
169
|
+
if abortable:
|
170
|
+
# run commands in the background, and poll a file to check for abort requests:
|
171
|
+
return dedent(
|
172
|
+
"""\
|
173
|
+
(
|
174
|
+
{commands}) &
|
175
|
+
|
176
|
+
pid=$!
|
177
|
+
while true
|
178
|
+
do
|
179
|
+
abort_file=$WK_PATH/artifacts/submissions/$SUB_IDX/abort_EARs.txt
|
180
|
+
is_abort=`sed "$(($EAR_ID + 1))q;d" $abort_file`
|
181
|
+
ps -p $pid > /dev/null
|
182
|
+
if [ $? == 1 ]; then
|
183
|
+
wait $pid
|
184
|
+
exitcode=$?
|
185
|
+
break
|
186
|
+
elif [ "$is_abort" = "1" ]; then
|
187
|
+
kill $pid
|
188
|
+
wait $pid 2>/dev/null
|
189
|
+
exitcode={abort_exit_code}
|
190
|
+
break
|
191
|
+
else
|
192
|
+
sleep 1 # TODO: TEMP: increase for production
|
193
|
+
fi
|
194
|
+
done
|
195
|
+
return $exitcode
|
196
|
+
"""
|
197
|
+
).format(commands=commands, abort_exit_code=ABORT_EXIT_CODE)
|
198
|
+
else:
|
199
|
+
# run commands in "foreground":
|
200
|
+
return dedent(
|
201
|
+
"""\
|
202
|
+
(
|
203
|
+
{commands})
|
204
|
+
"""
|
205
|
+
).format(commands=commands)
|
162
206
|
|
163
207
|
|
164
208
|
class WSLBash(Bash):
|
165
|
-
|
166
209
|
DEFAULT_WSL_EXE = "wsl"
|
167
210
|
|
168
211
|
JS_HEADER = Bash.JS_HEADER.replace(
|
169
212
|
"WK_PATH_ARG=$WK_PATH",
|
170
213
|
"WK_PATH_ARG=`wslpath -m $WK_PATH`",
|
214
|
+
).replace(
|
215
|
+
'--with-config log_file_path "`pwd`',
|
216
|
+
'--with-config log_file_path "$(wslpath -m `pwd`)',
|
171
217
|
)
|
172
218
|
|
173
219
|
def __init__(
|