hpcflow-new2 0.2.0a159__py3-none-any.whl → 0.2.0a160__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 +2 -0
- hpcflow/sdk/app.py +89 -15
- hpcflow/sdk/cli.py +18 -0
- hpcflow/sdk/cli_common.py +16 -0
- hpcflow/sdk/core/actions.py +14 -6
- hpcflow/sdk/core/command_files.py +4 -4
- hpcflow/sdk/core/element.py +15 -16
- hpcflow/sdk/core/run_dir_files.py +63 -0
- hpcflow/sdk/core/task.py +34 -35
- hpcflow/sdk/core/utils.py +37 -0
- hpcflow/sdk/core/workflow.py +144 -49
- hpcflow/sdk/demo/cli.py +12 -0
- hpcflow/sdk/log.py +2 -2
- hpcflow/sdk/persistence/base.py +140 -12
- hpcflow/sdk/persistence/json.py +84 -63
- hpcflow/sdk/persistence/pending.py +21 -7
- hpcflow/sdk/persistence/zarr.py +143 -108
- hpcflow/sdk/submission/jobscript.py +22 -4
- hpcflow/sdk/submission/shells/bash.py +2 -2
- hpcflow/sdk/submission/shells/powershell.py +2 -2
- hpcflow/sdk/submission/submission.py +20 -7
- hpcflow/tests/scripts/test_main_scripts.py +40 -0
- hpcflow/tests/unit/test_utils.py +28 -0
- {hpcflow_new2-0.2.0a159.dist-info → hpcflow_new2-0.2.0a160.dist-info}/METADATA +1 -1
- {hpcflow_new2-0.2.0a159.dist-info → hpcflow_new2-0.2.0a160.dist-info}/RECORD +28 -27
- {hpcflow_new2-0.2.0a159.dist-info → hpcflow_new2-0.2.0a160.dist-info}/WHEEL +0 -0
- {hpcflow_new2-0.2.0a159.dist-info → hpcflow_new2-0.2.0a160.dist-info}/entry_points.txt +0 -0
hpcflow/sdk/core/task.py
CHANGED
@@ -1283,6 +1283,7 @@ class WorkflowTask:
|
|
1283
1283
|
return self.template.num_element_sets
|
1284
1284
|
|
1285
1285
|
@property
|
1286
|
+
@TimeIt.decorator
|
1286
1287
|
def elements(self):
|
1287
1288
|
if self._elements is None:
|
1288
1289
|
self._elements = self.app.Elements(self)
|
@@ -1828,6 +1829,7 @@ class WorkflowTask:
|
|
1828
1829
|
param_src_updates = {}
|
1829
1830
|
|
1830
1831
|
count = 0
|
1832
|
+
# TODO: generator is an IO op here, can be pre-calculated/cached?
|
1831
1833
|
for act_idx, action in self.template.all_schema_actions():
|
1832
1834
|
log_common = (
|
1833
1835
|
f"for action {act_idx} of element iteration {element_iter.index} of "
|
@@ -2604,41 +2606,38 @@ class Elements:
|
|
2604
2606
|
def task(self):
|
2605
2607
|
return self._task
|
2606
2608
|
|
2607
|
-
|
2609
|
+
@TimeIt.decorator
|
2610
|
+
def _get_selection(self, selection: Union[int, slice, List[int]]) -> List[int]:
|
2611
|
+
"""Normalise an element selection into a list of element indices."""
|
2608
2612
|
if isinstance(selection, int):
|
2609
|
-
|
2613
|
+
lst = [selection]
|
2610
2614
|
|
2611
2615
|
elif isinstance(selection, slice):
|
2612
|
-
|
2613
|
-
stop = self.task.num_elements if stop is None else stop
|
2614
|
-
start = start or 0
|
2615
|
-
step = 1 if step is None else step
|
2616
|
+
lst = list(range(*selection.indices(self.task.num_elements)))
|
2616
2617
|
|
2618
|
+
elif isinstance(selection, list):
|
2619
|
+
lst = selection
|
2617
2620
|
else:
|
2618
2621
|
raise RuntimeError(
|
2619
|
-
f"{self.__class__.__name__} selection must be an `int
|
2620
|
-
f"
|
2622
|
+
f"{self.__class__.__name__} selection must be an `int`, `slice` object, "
|
2623
|
+
f"or list of `int`s, but received type {type(selection)}."
|
2621
2624
|
)
|
2622
|
-
|
2623
|
-
selection = slice(start, stop, step)
|
2624
|
-
length = len(range(*selection.indices(self.task.num_elements)))
|
2625
|
-
|
2626
|
-
return selection, length
|
2625
|
+
return lst
|
2627
2626
|
|
2628
2627
|
def __len__(self):
|
2629
2628
|
return self.task.num_elements
|
2630
2629
|
|
2631
2630
|
def __iter__(self):
|
2632
|
-
|
2633
|
-
for i in all_elems:
|
2631
|
+
for i in self.task.workflow.get_task_elements(self.task):
|
2634
2632
|
yield i
|
2635
2633
|
|
2634
|
+
@TimeIt.decorator
|
2636
2635
|
def __getitem__(
|
2637
2636
|
self,
|
2638
|
-
selection: Union[int, slice],
|
2637
|
+
selection: Union[int, slice, List[int]],
|
2639
2638
|
) -> Union[app.Element, List[app.Element]]:
|
2640
|
-
|
2641
|
-
elements = self.task.workflow.get_task_elements(self.task,
|
2639
|
+
idx_lst = self._get_selection(selection)
|
2640
|
+
elements = self.task.workflow.get_task_elements(self.task, idx_lst)
|
2642
2641
|
|
2643
2642
|
if isinstance(selection, int):
|
2644
2643
|
return elements[0]
|
@@ -2657,34 +2656,34 @@ class Parameters:
|
|
2657
2656
|
raise_on_unset: Optional[bool] = False
|
2658
2657
|
default: Optional[Any] = None
|
2659
2658
|
|
2660
|
-
|
2659
|
+
@TimeIt.decorator
|
2660
|
+
def _get_selection(self, selection: Union[int, slice, List[int]]) -> List[int]:
|
2661
|
+
"""Normalise an element selection into a list of element indices."""
|
2661
2662
|
if isinstance(selection, int):
|
2662
|
-
|
2663
|
+
lst = [selection]
|
2663
2664
|
|
2664
2665
|
elif isinstance(selection, slice):
|
2665
|
-
|
2666
|
-
stop = self.task.num_elements if stop is None else stop
|
2667
|
-
start = start or 0
|
2668
|
-
step = 1 if step is None else step
|
2666
|
+
lst = list(range(*selection.indices(self.task.num_elements)))
|
2669
2667
|
|
2668
|
+
elif isinstance(selection, list):
|
2669
|
+
lst = selection
|
2670
2670
|
else:
|
2671
2671
|
raise RuntimeError(
|
2672
|
-
f"{self.__class__.__name__} selection must be an `int
|
2673
|
-
f"
|
2672
|
+
f"{self.__class__.__name__} selection must be an `int`, `slice` object, "
|
2673
|
+
f"or list of `int`s, but received type {type(selection)}."
|
2674
2674
|
)
|
2675
|
-
|
2676
|
-
selection = slice(start, stop, step)
|
2677
|
-
length = len(range(*selection.indices(self.task.num_elements)))
|
2678
|
-
|
2679
|
-
return selection, length
|
2675
|
+
return lst
|
2680
2676
|
|
2681
2677
|
def __iter__(self):
|
2682
2678
|
for i in self.__getitem__(slice(None)):
|
2683
2679
|
yield i
|
2684
2680
|
|
2685
|
-
def __getitem__(
|
2686
|
-
|
2687
|
-
|
2681
|
+
def __getitem__(
|
2682
|
+
self,
|
2683
|
+
selection: Union[int, slice, List[int]],
|
2684
|
+
) -> Union[Any, List[Any]]:
|
2685
|
+
idx_lst = self._get_selection(selection)
|
2686
|
+
elements = self.task.workflow.get_task_elements(self.task, idx_lst)
|
2688
2687
|
if self.return_element_parameters:
|
2689
2688
|
params = [
|
2690
2689
|
self._app.ElementParameter(
|
@@ -2706,7 +2705,7 @@ class Parameters:
|
|
2706
2705
|
for i in elements
|
2707
2706
|
]
|
2708
2707
|
|
2709
|
-
if
|
2708
|
+
if isinstance(selection, int):
|
2710
2709
|
return params[0]
|
2711
2710
|
else:
|
2712
2711
|
return params
|
hpcflow/sdk/core/utils.py
CHANGED
@@ -834,3 +834,40 @@ def linspace_rect(
|
|
834
834
|
|
835
835
|
rect = np.hstack(stacked)
|
836
836
|
return rect
|
837
|
+
|
838
|
+
|
839
|
+
def dict_values_process_flat(d, callable):
|
840
|
+
"""
|
841
|
+
Return a copy of a dict, where the values are processed by a callable that is to
|
842
|
+
be called only once, and where the values may be single items or lists of items.
|
843
|
+
|
844
|
+
Examples
|
845
|
+
--------
|
846
|
+
d = {'a': 0, 'b': [1, 2], 'c': 5}
|
847
|
+
>>> dict_values_process_flat(d, callable=lambda x: [i + 1 for i in x])
|
848
|
+
{'a': 1, 'b': [2, 3], 'c': 6}
|
849
|
+
|
850
|
+
"""
|
851
|
+
flat = [] # values of `d`, flattened
|
852
|
+
is_multi = [] # whether a list, and the number of items to process
|
853
|
+
for i in d.values():
|
854
|
+
try:
|
855
|
+
flat.extend(i)
|
856
|
+
is_multi.append((True, len(i)))
|
857
|
+
except TypeError:
|
858
|
+
flat.append(i)
|
859
|
+
is_multi.append((False, 1))
|
860
|
+
|
861
|
+
processed = callable(flat)
|
862
|
+
|
863
|
+
out = {}
|
864
|
+
for idx_i, (m, k) in enumerate(zip(is_multi, d.keys())):
|
865
|
+
|
866
|
+
start_idx = sum(i[1] for i in is_multi[:idx_i])
|
867
|
+
end_idx = start_idx + m[1]
|
868
|
+
proc_idx_k = processed[slice(start_idx, end_idx)]
|
869
|
+
if not m[0]:
|
870
|
+
proc_idx_k = proc_idx_k[0]
|
871
|
+
out[k] = proc_idx_k
|
872
|
+
|
873
|
+
return out
|
hpcflow/sdk/core/workflow.py
CHANGED
@@ -492,6 +492,7 @@ class Workflow:
|
|
492
492
|
ts_fmt: Optional[str] = None,
|
493
493
|
ts_name_fmt: Optional[str] = None,
|
494
494
|
store_kwargs: Optional[Dict] = None,
|
495
|
+
status: Optional[Any] = None,
|
495
496
|
) -> app.Workflow:
|
496
497
|
"""Generate from a `WorkflowTemplate` object.
|
497
498
|
|
@@ -521,22 +522,38 @@ class Workflow:
|
|
521
522
|
store_kwargs
|
522
523
|
Keyword arguments to pass to the store's `write_empty_workflow` method.
|
523
524
|
"""
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
525
|
+
if status:
|
526
|
+
status.update("Generating empty workflow...")
|
527
|
+
try:
|
528
|
+
wk = cls._write_empty_workflow(
|
529
|
+
template=template,
|
530
|
+
path=path,
|
531
|
+
name=name,
|
532
|
+
overwrite=overwrite,
|
533
|
+
store=store,
|
534
|
+
ts_fmt=ts_fmt,
|
535
|
+
ts_name_fmt=ts_name_fmt,
|
536
|
+
store_kwargs=store_kwargs,
|
537
|
+
)
|
538
|
+
with wk._store.cached_load():
|
539
|
+
with wk.batch_update(is_workflow_creation=True):
|
540
|
+
for idx, task in enumerate(template.tasks):
|
541
|
+
if status:
|
542
|
+
status.update(
|
543
|
+
f"Adding task {idx + 1}/{len(template.tasks)} "
|
544
|
+
f"({task.name!r})..."
|
545
|
+
)
|
546
|
+
wk._add_task(task)
|
547
|
+
for idx, loop in enumerate(template.loops):
|
548
|
+
if status:
|
549
|
+
status.update(
|
550
|
+
f"Adding loop {idx + 1}/" f"{len(template.loops)}..."
|
551
|
+
)
|
552
|
+
wk._add_loop(loop)
|
553
|
+
except Exception:
|
554
|
+
if status:
|
555
|
+
status.stop()
|
556
|
+
raise
|
540
557
|
return wk
|
541
558
|
|
542
559
|
@classmethod
|
@@ -668,6 +685,7 @@ class Workflow:
|
|
668
685
|
ts_name_fmt: Optional[str] = None,
|
669
686
|
store_kwargs: Optional[Dict] = None,
|
670
687
|
variables: Optional[Dict[str, str]] = None,
|
688
|
+
status: Optional[Any] = None,
|
671
689
|
) -> app.Workflow:
|
672
690
|
"""Generate from a JSON file.
|
673
691
|
|
@@ -712,6 +730,7 @@ class Workflow:
|
|
712
730
|
ts_fmt,
|
713
731
|
ts_name_fmt,
|
714
732
|
store_kwargs,
|
733
|
+
status,
|
715
734
|
)
|
716
735
|
|
717
736
|
@classmethod
|
@@ -726,6 +745,7 @@ class Workflow:
|
|
726
745
|
ts_name_fmt: Optional[str] = None,
|
727
746
|
store_kwargs: Optional[Dict] = None,
|
728
747
|
variables: Optional[Dict[str, str]] = None,
|
748
|
+
status: Optional[Any] = None,
|
729
749
|
) -> app.Workflow:
|
730
750
|
"""Generate from a JSON string.
|
731
751
|
|
@@ -770,6 +790,7 @@ class Workflow:
|
|
770
790
|
ts_fmt,
|
771
791
|
ts_name_fmt,
|
772
792
|
store_kwargs,
|
793
|
+
status,
|
773
794
|
)
|
774
795
|
|
775
796
|
@classmethod
|
@@ -786,6 +807,7 @@ class Workflow:
|
|
786
807
|
ts_name_fmt: Optional[str] = None,
|
787
808
|
store_kwargs: Optional[Dict] = None,
|
788
809
|
variables: Optional[Dict[str, str]] = None,
|
810
|
+
status: Optional[Any] = None,
|
789
811
|
) -> app.Workflow:
|
790
812
|
"""Generate from either a YAML or JSON file, depending on the file extension.
|
791
813
|
|
@@ -821,11 +843,16 @@ class Workflow:
|
|
821
843
|
variables
|
822
844
|
String variables to substitute in the file given by `template_path`.
|
823
845
|
"""
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
846
|
+
try:
|
847
|
+
template = cls.app.WorkflowTemplate.from_file(
|
848
|
+
template_path,
|
849
|
+
template_format,
|
850
|
+
variables=variables,
|
851
|
+
)
|
852
|
+
except Exception:
|
853
|
+
if status:
|
854
|
+
status.stop()
|
855
|
+
raise
|
829
856
|
return cls.from_template(
|
830
857
|
template,
|
831
858
|
path,
|
@@ -835,6 +862,7 @@ class Workflow:
|
|
835
862
|
ts_fmt,
|
836
863
|
ts_name_fmt,
|
837
864
|
store_kwargs,
|
865
|
+
status,
|
838
866
|
)
|
839
867
|
|
840
868
|
@classmethod
|
@@ -1262,7 +1290,6 @@ class Workflow:
|
|
1262
1290
|
@TimeIt.decorator
|
1263
1291
|
def get_EARs_from_IDs(self, id_lst: Iterable[int]) -> List[app.ElementActionRun]:
|
1264
1292
|
"""Return element action run objects from a list of IDs."""
|
1265
|
-
|
1266
1293
|
self.app.persistence_logger.debug(f"get_EARs_from_IDs: id_lst={id_lst!r}")
|
1267
1294
|
|
1268
1295
|
store_EARs = self._store.get_EARs(id_lst)
|
@@ -1276,6 +1303,10 @@ class Workflow:
|
|
1276
1303
|
task_IDs = [i.task_ID for i in store_elems]
|
1277
1304
|
store_tasks = self._store.get_tasks_by_IDs(task_IDs)
|
1278
1305
|
|
1306
|
+
# to allow for bulk retrieval of elements/iterations
|
1307
|
+
element_idx_by_task = defaultdict(set)
|
1308
|
+
iter_idx_by_task_elem = defaultdict(lambda: defaultdict(set))
|
1309
|
+
|
1279
1310
|
index_paths = []
|
1280
1311
|
for rn, it, el, tk in zip(store_EARs, store_iters, store_elems, store_tasks):
|
1281
1312
|
act_idx = rn.action_idx
|
@@ -1291,12 +1322,25 @@ class Workflow:
|
|
1291
1322
|
"task_idx": tk.index,
|
1292
1323
|
}
|
1293
1324
|
)
|
1325
|
+
element_idx_by_task[tk.index].add(elem_idx)
|
1326
|
+
iter_idx_by_task_elem[tk.index][elem_idx].add(iter_idx)
|
1327
|
+
|
1328
|
+
# retrieve elements/iterations:
|
1329
|
+
iters_by_task_elem = defaultdict(lambda: defaultdict(dict))
|
1330
|
+
for task_idx, elem_idx in element_idx_by_task.items():
|
1331
|
+
elements = self.tasks[task_idx].elements[list(elem_idx)]
|
1332
|
+
for elem_i in elements:
|
1333
|
+
elem_i_iters_idx = iter_idx_by_task_elem[task_idx][elem_i.index]
|
1334
|
+
elem_iters = [elem_i.iterations[j] for j in elem_i_iters_idx]
|
1335
|
+
iters_by_task_elem[task_idx][elem_i.index].update(
|
1336
|
+
dict(zip(elem_i_iters_idx, elem_iters))
|
1337
|
+
)
|
1294
1338
|
|
1295
1339
|
objs = []
|
1296
1340
|
for idx_dat in index_paths:
|
1297
|
-
|
1298
|
-
|
1299
|
-
|
1341
|
+
iter_ = iters_by_task_elem[idx_dat["task_idx"]][idx_dat["elem_idx"]][
|
1342
|
+
idx_dat["iter_idx"]
|
1343
|
+
]
|
1300
1344
|
run = iter_.actions[idx_dat["action_idx"]].runs[idx_dat["run_idx"]]
|
1301
1345
|
objs.append(run)
|
1302
1346
|
|
@@ -1580,6 +1624,13 @@ class Workflow:
|
|
1580
1624
|
id_lst = list(range(num_params))
|
1581
1625
|
return self._store.get_parameters(id_lst, **kwargs)
|
1582
1626
|
|
1627
|
+
@TimeIt.decorator
|
1628
|
+
def get_all_parameter_sources(self, **kwargs: Dict) -> List[Dict]:
|
1629
|
+
"""Retrieve all store parameters."""
|
1630
|
+
num_params = self._store._get_num_total_parameters()
|
1631
|
+
id_lst = list(range(num_params))
|
1632
|
+
return self._store.get_parameter_sources(id_lst, **kwargs)
|
1633
|
+
|
1583
1634
|
@TimeIt.decorator
|
1584
1635
|
def get_all_parameter_data(self, **kwargs: Dict) -> Dict[int, Any]:
|
1585
1636
|
"""Retrieve all workflow parameter data."""
|
@@ -1759,10 +1810,14 @@ class Workflow:
|
|
1759
1810
|
return Path(self.path) / self._exec_dir_name
|
1760
1811
|
|
1761
1812
|
@TimeIt.decorator
|
1762
|
-
def get_task_elements(
|
1813
|
+
def get_task_elements(
|
1814
|
+
self,
|
1815
|
+
task: app.Task,
|
1816
|
+
idx_lst: Optional[List[int]] = None,
|
1817
|
+
) -> List[app.Element]:
|
1763
1818
|
return [
|
1764
1819
|
self.app.Element(task=task, **{k: v for k, v in i.items() if k != "task_ID"})
|
1765
|
-
for i in self._store.get_task_elements(task.insert_ID,
|
1820
|
+
for i in self._store.get_task_elements(task.insert_ID, idx_lst)
|
1766
1821
|
]
|
1767
1822
|
|
1768
1823
|
def set_EAR_submission_index(self, EAR_ID: int, sub_idx: int) -> None:
|
@@ -1929,6 +1984,7 @@ class Workflow:
|
|
1929
1984
|
for element in task.elements[:]:
|
1930
1985
|
yield element
|
1931
1986
|
|
1987
|
+
@TimeIt.decorator
|
1932
1988
|
def get_iteration_task_pathway(self):
|
1933
1989
|
pathway = []
|
1934
1990
|
for task in self.tasks:
|
@@ -1955,7 +2011,7 @@ class Workflow:
|
|
1955
2011
|
@TimeIt.decorator
|
1956
2012
|
def _submit(
|
1957
2013
|
self,
|
1958
|
-
status,
|
2014
|
+
status: Optional[Any] = None,
|
1959
2015
|
ignore_errors: Optional[bool] = False,
|
1960
2016
|
JS_parallelism: Optional[bool] = None,
|
1961
2017
|
print_stdout: Optional[bool] = False,
|
@@ -1967,10 +2023,12 @@ class Workflow:
|
|
1967
2023
|
# generate a new submission if there are no pending submissions:
|
1968
2024
|
pending = [i for i in self.submissions if i.needs_submit]
|
1969
2025
|
if not pending:
|
1970
|
-
status
|
2026
|
+
if status:
|
2027
|
+
status.update("Adding new submission...")
|
1971
2028
|
new_sub = self._add_submission(tasks=tasks, JS_parallelism=JS_parallelism)
|
1972
2029
|
if not new_sub:
|
1973
|
-
status
|
2030
|
+
if status:
|
2031
|
+
status.stop()
|
1974
2032
|
raise ValueError("No pending element action runs to submit!")
|
1975
2033
|
pending = [new_sub]
|
1976
2034
|
|
@@ -1980,7 +2038,8 @@ class Workflow:
|
|
1980
2038
|
|
1981
2039
|
# for direct execution the submission must be persistent at submit-time, because
|
1982
2040
|
# it will be read by a new instance of the app:
|
1983
|
-
status
|
2041
|
+
if status:
|
2042
|
+
status.update("Committing to the store...")
|
1984
2043
|
self._store._pending.commit_all()
|
1985
2044
|
|
1986
2045
|
# submit all pending submissions:
|
@@ -1988,7 +2047,8 @@ class Workflow:
|
|
1988
2047
|
submitted_js = {}
|
1989
2048
|
for sub in pending:
|
1990
2049
|
try:
|
1991
|
-
status
|
2050
|
+
if status:
|
2051
|
+
status.update(f"Preparing submission {sub.index}...")
|
1992
2052
|
sub_js_idx = sub.submit(
|
1993
2053
|
status=status,
|
1994
2054
|
ignore_errors=ignore_errors,
|
@@ -2010,6 +2070,8 @@ class Workflow:
|
|
2010
2070
|
add_to_known: Optional[bool] = True,
|
2011
2071
|
return_idx: Optional[bool] = False,
|
2012
2072
|
tasks: Optional[List[int]] = None,
|
2073
|
+
cancel: Optional[bool] = False,
|
2074
|
+
status: Optional[bool] = True,
|
2013
2075
|
) -> Dict[int, int]:
|
2014
2076
|
"""Submit the workflow for execution.
|
2015
2077
|
|
@@ -2036,39 +2098,52 @@ class Workflow:
|
|
2036
2098
|
List of task indices to include in the new submission if no submissions
|
2037
2099
|
already exist. By default all tasks are included if a new submission is
|
2038
2100
|
created.
|
2101
|
+
cancel
|
2102
|
+
Immediately cancel the submission. Useful for testing and benchmarking.
|
2103
|
+
status
|
2104
|
+
If True, display a live status to track submission progress.
|
2039
2105
|
"""
|
2040
2106
|
|
2041
|
-
|
2042
|
-
|
2043
|
-
|
2107
|
+
if status:
|
2108
|
+
console = rich.console.Console()
|
2109
|
+
status = console.status("Submitting workflow...")
|
2110
|
+
status.start()
|
2044
2111
|
|
2045
2112
|
with self._store.cached_load():
|
2046
2113
|
if not self._store.is_submittable:
|
2047
|
-
status
|
2114
|
+
if status:
|
2115
|
+
status.stop()
|
2048
2116
|
raise NotImplementedError("The workflow is not submittable.")
|
2049
2117
|
with self.batch_update():
|
2050
2118
|
# commit updates before raising exception:
|
2051
2119
|
try:
|
2052
|
-
|
2053
|
-
|
2054
|
-
|
2055
|
-
|
2056
|
-
|
2057
|
-
|
2058
|
-
|
2059
|
-
|
2120
|
+
with self._store.cache_ctx():
|
2121
|
+
exceptions, submitted_js = self._submit(
|
2122
|
+
ignore_errors=ignore_errors,
|
2123
|
+
JS_parallelism=JS_parallelism,
|
2124
|
+
print_stdout=print_stdout,
|
2125
|
+
status=status,
|
2126
|
+
add_to_known=add_to_known,
|
2127
|
+
tasks=tasks,
|
2128
|
+
)
|
2060
2129
|
except Exception:
|
2061
|
-
status
|
2130
|
+
if status:
|
2131
|
+
status.stop()
|
2062
2132
|
raise
|
2063
2133
|
|
2064
2134
|
if exceptions:
|
2065
2135
|
msg = "\n" + "\n\n".join([i.message for i in exceptions])
|
2066
|
-
status
|
2136
|
+
if status:
|
2137
|
+
status.stop()
|
2067
2138
|
raise WorkflowSubmissionFailure(msg)
|
2068
2139
|
|
2069
|
-
status
|
2140
|
+
if status:
|
2141
|
+
status.stop()
|
2142
|
+
|
2143
|
+
if cancel:
|
2144
|
+
self.cancel()
|
2070
2145
|
|
2071
|
-
|
2146
|
+
elif wait:
|
2072
2147
|
self.wait(submitted_js)
|
2073
2148
|
|
2074
2149
|
if return_idx:
|
@@ -2281,6 +2356,7 @@ class Workflow:
|
|
2281
2356
|
)
|
2282
2357
|
self._abort_run_ID(submission_idx, run.id_)
|
2283
2358
|
|
2359
|
+
@TimeIt.decorator
|
2284
2360
|
def cancel(self, hard=False):
|
2285
2361
|
"""Cancel any running jobscripts."""
|
2286
2362
|
for sub in self.submissions:
|
@@ -2328,6 +2404,7 @@ class Workflow:
|
|
2328
2404
|
|
2329
2405
|
return self.submissions[new_idx]
|
2330
2406
|
|
2407
|
+
@TimeIt.decorator
|
2331
2408
|
def resolve_jobscripts(
|
2332
2409
|
self, tasks: Optional[List[int]] = None
|
2333
2410
|
) -> List[app.Jobscript]:
|
@@ -2344,6 +2421,7 @@ class Workflow:
|
|
2344
2421
|
|
2345
2422
|
return js_objs
|
2346
2423
|
|
2424
|
+
@TimeIt.decorator
|
2347
2425
|
def _resolve_singular_jobscripts(
|
2348
2426
|
self, tasks: Optional[List[int]] = None
|
2349
2427
|
) -> Tuple[Dict[int, Dict], Dict]:
|
@@ -2362,6 +2440,10 @@ class Workflow:
|
|
2362
2440
|
if not tasks:
|
2363
2441
|
tasks = list(range(self.num_tasks))
|
2364
2442
|
|
2443
|
+
if self._store.use_cache:
|
2444
|
+
# pre-cache parameter sources (used in `EAR.get_EAR_dependencies`):
|
2445
|
+
self.get_all_parameter_sources()
|
2446
|
+
|
2365
2447
|
submission_jobscripts = {}
|
2366
2448
|
all_element_deps = {}
|
2367
2449
|
|
@@ -2408,6 +2490,19 @@ class Workflow:
|
|
2408
2490
|
"resource_hash": res_hash[js_dat["resources"]],
|
2409
2491
|
"dependencies": {},
|
2410
2492
|
}
|
2493
|
+
|
2494
|
+
all_EAR_IDs = []
|
2495
|
+
for js_elem_idx, (elem_idx, act_indices) in enumerate(
|
2496
|
+
js_dat["elements"].items()
|
2497
|
+
):
|
2498
|
+
for act_idx in act_indices:
|
2499
|
+
EAR_ID_i = EAR_map[act_idx, elem_idx].item()
|
2500
|
+
all_EAR_IDs.append(EAR_ID_i)
|
2501
|
+
js_act_idx = task_actions.index([task.insert_ID, act_idx, 0])
|
2502
|
+
js_i["EAR_ID"][js_act_idx][js_elem_idx] = EAR_ID_i
|
2503
|
+
|
2504
|
+
all_EAR_objs = dict(zip(all_EAR_IDs, self.get_EARs_from_IDs(all_EAR_IDs)))
|
2505
|
+
|
2411
2506
|
for js_elem_idx, (elem_idx, act_indices) in enumerate(
|
2412
2507
|
js_dat["elements"].items()
|
2413
2508
|
):
|
@@ -2419,7 +2514,7 @@ class Workflow:
|
|
2419
2514
|
js_i["EAR_ID"][js_act_idx][js_elem_idx] = EAR_ID_i
|
2420
2515
|
|
2421
2516
|
# get indices of EARs that this element depends on:
|
2422
|
-
EAR_objs =
|
2517
|
+
EAR_objs = [all_EAR_objs[k] for k in all_EAR_IDs]
|
2423
2518
|
EAR_deps = [i.get_EAR_dependencies() for i in EAR_objs]
|
2424
2519
|
EAR_deps_flat = [j for i in EAR_deps for j in i]
|
2425
2520
|
EAR_deps_EAR_idx = [
|
hpcflow/sdk/demo/cli.py
CHANGED
@@ -17,6 +17,9 @@ from hpcflow.sdk.cli_common import (
|
|
17
17
|
add_to_known_opt,
|
18
18
|
print_idx_opt,
|
19
19
|
tasks_opt,
|
20
|
+
cancel_opt,
|
21
|
+
submit_status_opt,
|
22
|
+
make_status_opt,
|
20
23
|
)
|
21
24
|
|
22
25
|
|
@@ -100,6 +103,7 @@ def get_demo_workflow_CLI(app):
|
|
100
103
|
@ts_fmt_option
|
101
104
|
@ts_name_fmt_option
|
102
105
|
@variables_option
|
106
|
+
@make_status_opt
|
103
107
|
def make_demo_workflow(
|
104
108
|
workflow_name,
|
105
109
|
format,
|
@@ -110,6 +114,7 @@ def get_demo_workflow_CLI(app):
|
|
110
114
|
ts_fmt=None,
|
111
115
|
ts_name_fmt=None,
|
112
116
|
variables=None,
|
117
|
+
status=True,
|
113
118
|
):
|
114
119
|
wk = app.make_demo_workflow(
|
115
120
|
workflow_name=workflow_name,
|
@@ -121,6 +126,7 @@ def get_demo_workflow_CLI(app):
|
|
121
126
|
ts_fmt=ts_fmt,
|
122
127
|
ts_name_fmt=ts_name_fmt,
|
123
128
|
variables=dict(variables),
|
129
|
+
status=status,
|
124
130
|
)
|
125
131
|
click.echo(wk.path)
|
126
132
|
|
@@ -139,6 +145,8 @@ def get_demo_workflow_CLI(app):
|
|
139
145
|
@add_to_known_opt
|
140
146
|
@print_idx_opt
|
141
147
|
@tasks_opt
|
148
|
+
@cancel_opt
|
149
|
+
@submit_status_opt
|
142
150
|
def make_and_submit_demo_workflow(
|
143
151
|
workflow_name,
|
144
152
|
format,
|
@@ -154,6 +162,8 @@ def get_demo_workflow_CLI(app):
|
|
154
162
|
add_to_known=True,
|
155
163
|
print_idx=False,
|
156
164
|
tasks=None,
|
165
|
+
cancel=False,
|
166
|
+
status=True,
|
157
167
|
):
|
158
168
|
out = app.make_and_submit_demo_workflow(
|
159
169
|
workflow_name=workflow_name,
|
@@ -170,6 +180,8 @@ def get_demo_workflow_CLI(app):
|
|
170
180
|
add_to_known=add_to_known,
|
171
181
|
return_idx=print_idx,
|
172
182
|
tasks=tasks,
|
183
|
+
cancel=cancel,
|
184
|
+
status=status,
|
173
185
|
)
|
174
186
|
if print_idx:
|
175
187
|
click.echo(out)
|
hpcflow/sdk/log.py
CHANGED
@@ -97,7 +97,7 @@ class TimeIt:
|
|
97
97
|
max_str = f"{v['max']:10.6f}" if number > 1 else f"{f'-':^12s}"
|
98
98
|
stddev_str = f"({v['stddev']:8.6f})" if number > 1 else f"{f' ':^10s}"
|
99
99
|
out.append(
|
100
|
-
f"{k_str:.<
|
100
|
+
f"{k_str:.<80s} {v['sum']:12.6f} "
|
101
101
|
f"{v['mean']:10.6f} {stddev_str} {number:8d} "
|
102
102
|
f"{min_str} {max_str} "
|
103
103
|
)
|
@@ -114,7 +114,7 @@ class TimeIt:
|
|
114
114
|
summary = cls.summarise()
|
115
115
|
|
116
116
|
out = [
|
117
|
-
f"{'function':^
|
117
|
+
f"{'function':^80s} {'sum /s':^12s} {'mean (stddev) /s':^20s} {'N':^8s} "
|
118
118
|
f"{'min /s':^12s} {'max /s':^12s}"
|
119
119
|
]
|
120
120
|
out += _format_nodes(summary)
|