hpcflow-new2 0.2.0a158__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/app.py +0 -3
- hpcflow/sdk/__init__.py +2 -0
- hpcflow/sdk/app.py +91 -18
- hpcflow/sdk/cli.py +18 -0
- hpcflow/sdk/cli_common.py +16 -0
- hpcflow/sdk/config/config.py +0 -4
- hpcflow/sdk/core/actions.py +20 -7
- hpcflow/sdk/core/command_files.py +4 -4
- hpcflow/sdk/core/element.py +15 -16
- hpcflow/sdk/core/rule.py +2 -0
- hpcflow/sdk/core/run_dir_files.py +63 -0
- hpcflow/sdk/core/task.py +34 -35
- hpcflow/sdk/core/utils.py +37 -15
- hpcflow/sdk/core/workflow.py +147 -49
- hpcflow/sdk/data/config_schema.yaml +0 -6
- hpcflow/sdk/demo/cli.py +12 -0
- hpcflow/sdk/log.py +2 -2
- hpcflow/sdk/persistence/base.py +142 -12
- hpcflow/sdk/persistence/json.py +84 -63
- hpcflow/sdk/persistence/pending.py +21 -7
- hpcflow/sdk/persistence/utils.py +2 -1
- hpcflow/sdk/persistence/zarr.py +143 -108
- hpcflow/sdk/runtime.py +0 -12
- hpcflow/sdk/submission/jobscript.py +25 -4
- hpcflow/sdk/submission/schedulers/sge.py +3 -0
- hpcflow/sdk/submission/schedulers/slurm.py +3 -0
- hpcflow/sdk/submission/shells/bash.py +2 -2
- hpcflow/sdk/submission/shells/powershell.py +2 -2
- hpcflow/sdk/submission/submission.py +24 -7
- hpcflow/tests/scripts/test_main_scripts.py +40 -0
- hpcflow/tests/unit/test_utils.py +28 -0
- {hpcflow_new2-0.2.0a158.dist-info → hpcflow_new2-0.2.0a160.dist-info}/METADATA +1 -2
- {hpcflow_new2-0.2.0a158.dist-info → hpcflow_new2-0.2.0a160.dist-info}/RECORD +36 -35
- {hpcflow_new2-0.2.0a158.dist-info → hpcflow_new2-0.2.0a160.dist-info}/WHEEL +0 -0
- {hpcflow_new2-0.2.0a158.dist-info → hpcflow_new2-0.2.0a160.dist-info}/entry_points.txt +0 -0
hpcflow/sdk/core/element.py
CHANGED
@@ -13,9 +13,11 @@ from hpcflow.sdk.core.json_like import ChildObjectSpec, JSONLike
|
|
13
13
|
from hpcflow.sdk.core.parallel import ParallelMode
|
14
14
|
from hpcflow.sdk.core.utils import (
|
15
15
|
check_valid_py_identifier,
|
16
|
+
dict_values_process_flat,
|
16
17
|
get_enum_by_name_or_val,
|
17
18
|
split_param_label,
|
18
19
|
)
|
20
|
+
from hpcflow.sdk.log import TimeIt
|
19
21
|
from hpcflow.sdk.submission.shells import get_shell
|
20
22
|
|
21
23
|
|
@@ -500,6 +502,7 @@ class ElementIteration:
|
|
500
502
|
if i.startswith(prefix)
|
501
503
|
)
|
502
504
|
|
505
|
+
@TimeIt.decorator
|
503
506
|
def get_data_idx(
|
504
507
|
self,
|
505
508
|
path: str = None,
|
@@ -538,6 +541,7 @@ class ElementIteration:
|
|
538
541
|
|
539
542
|
return copy.deepcopy(data_idx)
|
540
543
|
|
544
|
+
@TimeIt.decorator
|
541
545
|
def get_parameter_sources(
|
542
546
|
self,
|
543
547
|
path: str = None,
|
@@ -555,24 +559,16 @@ class ElementIteration:
|
|
555
559
|
ID.
|
556
560
|
"""
|
557
561
|
data_idx = self.get_data_idx(path, action_idx, run_idx)
|
558
|
-
out = {}
|
559
|
-
for k, v in data_idx.items():
|
560
|
-
is_multi = False
|
561
|
-
if isinstance(v, list):
|
562
|
-
is_multi = True
|
563
|
-
else:
|
564
|
-
v = [v]
|
565
562
|
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
if not is_multi:
|
572
|
-
sources_k = src
|
573
|
-
|
574
|
-
out[k] = sources_k
|
563
|
+
# the value associated with `repeats.*` is the repeats index, not a parameter ID:
|
564
|
+
for k in list(data_idx.keys()):
|
565
|
+
if k.startswith("repeats."):
|
566
|
+
data_idx.pop(k)
|
575
567
|
|
568
|
+
out = dict_values_process_flat(
|
569
|
+
data_idx,
|
570
|
+
callable=self.workflow.get_parameter_sources,
|
571
|
+
)
|
576
572
|
task_key = "task_insert_ID"
|
577
573
|
|
578
574
|
if use_task_index:
|
@@ -631,6 +627,7 @@ class ElementIteration:
|
|
631
627
|
|
632
628
|
return out
|
633
629
|
|
630
|
+
@TimeIt.decorator
|
634
631
|
def get(
|
635
632
|
self,
|
636
633
|
path: str = None,
|
@@ -856,6 +853,7 @@ class ElementIteration:
|
|
856
853
|
out[res_i.scope.to_string()] = res_i._get_value()
|
857
854
|
return out
|
858
855
|
|
856
|
+
@TimeIt.decorator
|
859
857
|
def get_resources(self, action: app.Action, set_defaults: bool = False) -> Dict:
|
860
858
|
"""Resolve specific resources for the specified action of this iteration,
|
861
859
|
considering all applicable scopes.
|
@@ -998,6 +996,7 @@ class Element:
|
|
998
996
|
return self._iteration_IDs
|
999
997
|
|
1000
998
|
@property
|
999
|
+
@TimeIt.decorator
|
1001
1000
|
def iterations(self) -> Dict[app.ElementAction]:
|
1002
1001
|
# TODO: fix this
|
1003
1002
|
if self._iteration_objs is None:
|
hpcflow/sdk/core/rule.py
CHANGED
@@ -7,6 +7,7 @@ from valida.rules import Rule as ValidaRule
|
|
7
7
|
from hpcflow.sdk import app
|
8
8
|
from hpcflow.sdk.core.json_like import JSONLike
|
9
9
|
from hpcflow.sdk.core.utils import get_in_container
|
10
|
+
from hpcflow.sdk.log import TimeIt
|
10
11
|
|
11
12
|
|
12
13
|
class Rule(JSONLike):
|
@@ -68,6 +69,7 @@ class Rule(JSONLike):
|
|
68
69
|
else:
|
69
70
|
return False
|
70
71
|
|
72
|
+
@TimeIt.decorator
|
71
73
|
def test(
|
72
74
|
self,
|
73
75
|
element_like: Union[app.ElementIteration, app.ElementActionRun],
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import re
|
2
|
+
from hpcflow.sdk.core.utils import JSONLikeDirSnapShot
|
3
|
+
|
4
|
+
|
5
|
+
class RunDirAppFiles:
|
6
|
+
"""A class to encapsulate the naming/recognition of app-created files within run
|
7
|
+
directories."""
|
8
|
+
|
9
|
+
_app_attr = "app"
|
10
|
+
|
11
|
+
CMD_FILES_RE_PATTERN = r"js_\d+_act_\d+\.?\w*"
|
12
|
+
|
13
|
+
@classmethod
|
14
|
+
def get_log_file_name(cls):
|
15
|
+
"""File name for the app log file."""
|
16
|
+
return f"{cls.app.package_name}.log"
|
17
|
+
|
18
|
+
@classmethod
|
19
|
+
def get_std_file_name(cls):
|
20
|
+
"""File name for stdout and stderr streams from the app."""
|
21
|
+
return f"{cls.app.package_name}_std.txt"
|
22
|
+
|
23
|
+
@staticmethod
|
24
|
+
def get_run_file_prefix(js_idx: int, js_action_idx: int):
|
25
|
+
return f"js_{js_idx}_act_{js_action_idx}"
|
26
|
+
|
27
|
+
@classmethod
|
28
|
+
def get_commands_file_name(cls, js_idx: int, js_action_idx: int, shell):
|
29
|
+
return cls.get_run_file_prefix(js_idx, js_action_idx) + shell.JS_EXT
|
30
|
+
|
31
|
+
@classmethod
|
32
|
+
def get_run_param_dump_file_prefix(cls, js_idx: int, js_action_idx: int):
|
33
|
+
"""Get the prefix to a file in the run directory that the app will dump parameter
|
34
|
+
data to."""
|
35
|
+
return cls.get_run_file_prefix(js_idx, js_action_idx) + "_inputs"
|
36
|
+
|
37
|
+
@classmethod
|
38
|
+
def get_run_param_load_file_prefix(cls, js_idx: int, js_action_idx: int):
|
39
|
+
"""Get the prefix to a file in the run directory that the app will load parameter
|
40
|
+
data from."""
|
41
|
+
return cls.get_run_file_prefix(js_idx, js_action_idx) + "_outputs"
|
42
|
+
|
43
|
+
@classmethod
|
44
|
+
def take_snapshot(cls):
|
45
|
+
"""Take a JSONLikeDirSnapShot, and process to ignore files created by the app.
|
46
|
+
|
47
|
+
This includes command files that are invoked by jobscripts, the app log file, and
|
48
|
+
the app standard out/error file.
|
49
|
+
|
50
|
+
"""
|
51
|
+
snapshot = JSONLikeDirSnapShot()
|
52
|
+
snapshot.take(".")
|
53
|
+
ss_js = snapshot.to_json_like()
|
54
|
+
ss_js.pop("root_path") # always the current working directory of the run
|
55
|
+
for k in list(ss_js["data"].keys()):
|
56
|
+
if (
|
57
|
+
k == cls.get_log_file_name()
|
58
|
+
or k == cls.get_std_file_name()
|
59
|
+
or re.match(cls.CMD_FILES_RE_PATTERN, k)
|
60
|
+
):
|
61
|
+
ss_js["data"].pop(k)
|
62
|
+
|
63
|
+
return ss_js
|
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
@@ -20,7 +20,6 @@ import fsspec
|
|
20
20
|
import numpy as np
|
21
21
|
|
22
22
|
from ruamel.yaml import YAML
|
23
|
-
import sentry_sdk
|
24
23
|
from watchdog.utils.dirsnapshot import DirectorySnapshot
|
25
24
|
|
26
25
|
from hpcflow.sdk.core.errors import (
|
@@ -347,20 +346,6 @@ class Singleton(type):
|
|
347
346
|
return cls._instances[cls]
|
348
347
|
|
349
348
|
|
350
|
-
@contextlib.contextmanager
|
351
|
-
def sentry_wrap(name, transaction_op=None, span_op=None):
|
352
|
-
if not transaction_op:
|
353
|
-
transaction_op = name
|
354
|
-
if not span_op:
|
355
|
-
span_op = name
|
356
|
-
try:
|
357
|
-
with sentry_sdk.start_transaction(op=transaction_op, name=name):
|
358
|
-
with sentry_sdk.start_span(op=span_op) as span:
|
359
|
-
yield span
|
360
|
-
finally:
|
361
|
-
sentry_sdk.flush() # avoid queue message on stdout
|
362
|
-
|
363
|
-
|
364
349
|
def capitalise_first_letter(chars):
|
365
350
|
return chars[0].upper() + chars[1:]
|
366
351
|
|
@@ -849,3 +834,40 @@ def linspace_rect(
|
|
849
834
|
|
850
835
|
rect = np.hstack(stacked)
|
851
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
|