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.
Files changed (38) hide show
  1. hpcflow/_version.py +1 -1
  2. hpcflow/sdk/__init__.py +1 -1
  3. hpcflow/sdk/api.py +1 -1
  4. hpcflow/sdk/app.py +20 -11
  5. hpcflow/sdk/cli.py +34 -59
  6. hpcflow/sdk/core/__init__.py +13 -1
  7. hpcflow/sdk/core/actions.py +235 -126
  8. hpcflow/sdk/core/command_files.py +32 -24
  9. hpcflow/sdk/core/element.py +110 -114
  10. hpcflow/sdk/core/errors.py +57 -0
  11. hpcflow/sdk/core/loop.py +18 -34
  12. hpcflow/sdk/core/parameters.py +5 -3
  13. hpcflow/sdk/core/task.py +135 -131
  14. hpcflow/sdk/core/task_schema.py +11 -4
  15. hpcflow/sdk/core/utils.py +110 -2
  16. hpcflow/sdk/core/workflow.py +964 -676
  17. hpcflow/sdk/data/template_components/environments.yaml +0 -44
  18. hpcflow/sdk/data/template_components/task_schemas.yaml +52 -10
  19. hpcflow/sdk/persistence/__init__.py +21 -33
  20. hpcflow/sdk/persistence/base.py +1340 -458
  21. hpcflow/sdk/persistence/json.py +424 -546
  22. hpcflow/sdk/persistence/pending.py +563 -0
  23. hpcflow/sdk/persistence/store_resource.py +131 -0
  24. hpcflow/sdk/persistence/utils.py +57 -0
  25. hpcflow/sdk/persistence/zarr.py +852 -841
  26. hpcflow/sdk/submission/jobscript.py +133 -112
  27. hpcflow/sdk/submission/shells/bash.py +62 -16
  28. hpcflow/sdk/submission/shells/powershell.py +87 -16
  29. hpcflow/sdk/submission/submission.py +59 -35
  30. hpcflow/tests/unit/test_element.py +4 -9
  31. hpcflow/tests/unit/test_persistence.py +218 -0
  32. hpcflow/tests/unit/test_task.py +11 -12
  33. hpcflow/tests/unit/test_utils.py +82 -0
  34. hpcflow/tests/unit/test_workflow.py +3 -1
  35. {hpcflow_new2-0.2.0a50.dist-info → hpcflow_new2-0.2.0a52.dist-info}/METADATA +3 -1
  36. {hpcflow_new2-0.2.0a50.dist-info → hpcflow_new2-0.2.0a52.dist-info}/RECORD +38 -34
  37. {hpcflow_new2-0.2.0a50.dist-info → hpcflow_new2-0.2.0a52.dist-info}/WHEEL +0 -0
  38. {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
- EAR_idx_map = np.empty(
49
- shape=arr_shape,
50
- dtype=[("EAR_idx", np.int32), ("run_idx", np.int32), ("iteration_idx", np.int32)],
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
- EAR_idx_map[:] = (none_val, none_val, none_val) # TODO: add iteration_idx as well
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
- EAR_idx_map[act_idx, element.index] = (
72
- run.index,
73
- run.run_idx,
74
- iter_i.index,
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
- EAR_idx_map,
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["EARs"]:
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
- dep_j_task_iID = EAR_dep_j[0]
184
- dep_j_elem_idx = EAR_dep_j[1]
185
- js_elem_idx_k = js_k["task_elements"][dep_j_task_iID].index(
186
- dep_j_elem_idx
187
- )
188
- jobscript_deps[js_idx][js_k_idx]["js_element_mapping"][
189
- js_elem_idx_i
190
- ].append(js_elem_idx_k)
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]["EAR_idx"].shape[1]
199
- js_k_num_js_elements = jobscripts[js_k_idx]["EAR_idx"].shape[1]
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(set(i[0] for i in deps_j["js_element_mapping"].values()))
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(js_j["task_loop_idx"])
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["EAR_idx"] = np.vstack((js_j["EAR_idx"], js["EAR_idx"]))
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
- EAR_idx: NDArray,
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._EAR_idx = EAR_idx
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["EAR_idx"] = dct["EAR_idx"].tolist()
359
- dct["EARs"] = [[list(k), list(v)] for k, v in dct["EARs"].items()]
360
- if dct.get("scheduler_version_info"):
361
- dct["scheduler_version_info"] = list(dct["scheduler_version_info"])
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["EAR_idx"] = np.array(json_like["EAR_idx"])
367
- json_like["EARs"] = {tuple(i[0]): tuple(i[1]) for i in json_like["EARs"]}
368
- if json_like.get("scheduler_version_info"):
369
- json_like["scheduler_version_info"] = tuple(
370
- json_like["scheduler_version_info"]
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
- return self._EARs
407
+ # @property
408
+ # def EARs(self):
409
+ # return self._EARs
396
410
 
397
411
  @property
398
- def EAR_idx(self):
399
- return self._EAR_idx
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 scheduler_version_info(self):
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.EAR_idx.shape[0]
453
+ return self.EAR_ID.shape[0]
440
454
 
441
455
  @property
442
456
  def num_elements(self):
443
- return self.EAR_idx.shape[1]
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.features.EAR_parallelism
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 need_EAR_file_name(self):
506
- return f"js_{self.index}_need_EARs.txt"
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 need_EAR_file_path(self):
518
- return self.submission.path / self.need_EAR_file_name
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
- task_insert_IDs = np.empty_like(self.EAR_idx)
554
- task_insert_IDs[:] = np.array([i[0] for i in self.task_actions]).reshape(
555
- (len(self.task_actions), 1)
556
- )
557
- return task_insert_IDs
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.EAR_idx)
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
- element_idx = np.empty_like(self.EAR_idx)
568
- for task_iID, elem_idx in self.task_elements.items():
569
- rows_idx = [
570
- idx for idx, i in enumerate(self.task_actions) if i[0] == task_iID
571
- ]
572
- element_idx[rows_idx] = elem_idx
573
- return element_idx
574
-
575
- def get_EAR_run_idx_array(self):
576
- task_insert_ID_arr = self.get_task_insert_IDs_array()
577
- element_idx = self.get_task_element_idx_array()
578
- run_idx = np.empty_like(self.EAR_idx)
579
- for js_act_idx in range(self.num_actions):
580
- for js_elem_idx in range(self.num_elements):
581
- EAR_idx_i = self.EAR_idx[js_act_idx, js_elem_idx]
582
- task_iID_i = task_insert_ID_arr[js_act_idx, js_elem_idx]
583
- elem_idx_i = element_idx[js_act_idx, js_elem_idx]
584
- (iter_idx_i, act_idx_i, run_idx_i) = self.EARs[
585
- (task_iID_i, elem_idx_i, EAR_idx_i)
586
- ]
587
- run_idx[js_act_idx, js_elem_idx] = run_idx_i
588
- return run_idx
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.EAR_idx.shape,
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.EAR_idx[js_act_idx, js_elem_idx]
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 write_need_EARs_file(self):
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.need_EAR_file_path.open(mode="wt", newline="\n") as fp:
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.EAR_idx != -1).T,
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.need_EAR_file_name,
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 make_artifact_dirs(self, task_artifacts_path):
738
- task_insert_ID_arr = self.get_task_insert_IDs_array()
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
- t_iID = task_insert_ID_arr[js_act_idx, js_elem_idx].item()
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
- e_idx = element_idx[js_act_idx, js_elem_idx].item()
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(task_artifacts_path, task_dir, elem_dir, run_dir)
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(task_artifacts_path)
778
- self.write_need_EARs_file()
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
- elem_need_EARs=`sed "$((${{JS_elem_idx}} + 1))q;d" $EAR_ID_FILE`
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
- need_EAR="$(cut -d'{EAR_files_delimiter}' -f $(($JS_act_idx + 1)) <<< $elem_need_EARs)"
64
- if [ "$need_act" = "0" ]; then
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 $JS_elem_idx $JS_act_idx
72
- {workflow_app_alias} internal workflow $WK_PATH_ARG set-ear-start $SUB_IDX $JS_IDX $JS_elem_idx $JS_act_idx
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
- {workflow_app_alias} internal workflow $WK_PATH_ARG set-ear-end $SUB_IDX $JS_IDX $JS_elem_idx $JS_act_idx
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(self, workflow_app_alias, param_name, shell_var_name):
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" $SUB_IDX $JS_IDX $JS_elem_idx $JS_act_idx"
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
- return dedent(
157
- """\
158
- (
159
- {commands})
160
- """
161
- ).format(commands=commands)
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__(