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.
Files changed (36) hide show
  1. hpcflow/_version.py +1 -1
  2. hpcflow/app.py +0 -3
  3. hpcflow/sdk/__init__.py +2 -0
  4. hpcflow/sdk/app.py +91 -18
  5. hpcflow/sdk/cli.py +18 -0
  6. hpcflow/sdk/cli_common.py +16 -0
  7. hpcflow/sdk/config/config.py +0 -4
  8. hpcflow/sdk/core/actions.py +20 -7
  9. hpcflow/sdk/core/command_files.py +4 -4
  10. hpcflow/sdk/core/element.py +15 -16
  11. hpcflow/sdk/core/rule.py +2 -0
  12. hpcflow/sdk/core/run_dir_files.py +63 -0
  13. hpcflow/sdk/core/task.py +34 -35
  14. hpcflow/sdk/core/utils.py +37 -15
  15. hpcflow/sdk/core/workflow.py +147 -49
  16. hpcflow/sdk/data/config_schema.yaml +0 -6
  17. hpcflow/sdk/demo/cli.py +12 -0
  18. hpcflow/sdk/log.py +2 -2
  19. hpcflow/sdk/persistence/base.py +142 -12
  20. hpcflow/sdk/persistence/json.py +84 -63
  21. hpcflow/sdk/persistence/pending.py +21 -7
  22. hpcflow/sdk/persistence/utils.py +2 -1
  23. hpcflow/sdk/persistence/zarr.py +143 -108
  24. hpcflow/sdk/runtime.py +0 -12
  25. hpcflow/sdk/submission/jobscript.py +25 -4
  26. hpcflow/sdk/submission/schedulers/sge.py +3 -0
  27. hpcflow/sdk/submission/schedulers/slurm.py +3 -0
  28. hpcflow/sdk/submission/shells/bash.py +2 -2
  29. hpcflow/sdk/submission/shells/powershell.py +2 -2
  30. hpcflow/sdk/submission/submission.py +24 -7
  31. hpcflow/tests/scripts/test_main_scripts.py +40 -0
  32. hpcflow/tests/unit/test_utils.py +28 -0
  33. {hpcflow_new2-0.2.0a158.dist-info → hpcflow_new2-0.2.0a160.dist-info}/METADATA +1 -2
  34. {hpcflow_new2-0.2.0a158.dist-info → hpcflow_new2-0.2.0a160.dist-info}/RECORD +36 -35
  35. {hpcflow_new2-0.2.0a158.dist-info → hpcflow_new2-0.2.0a160.dist-info}/WHEEL +0 -0
  36. {hpcflow_new2-0.2.0a158.dist-info → hpcflow_new2-0.2.0a160.dist-info}/entry_points.txt +0 -0
@@ -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
- sources_k = []
567
- for dat_idx_i in v:
568
- src = self.workflow.get_parameter_source(dat_idx_i)
569
- sources_k.append(src)
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
- def _get_selection(self, selection):
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
- start, stop, step = selection, selection + 1, 1
2613
+ lst = [selection]
2610
2614
 
2611
2615
  elif isinstance(selection, slice):
2612
- start, stop, step = selection.start, selection.stop, selection.step
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` or a `slice` "
2620
- f"object, but received type {type(selection)}."
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
- all_elems = self.task.workflow.get_task_elements(self.task, slice(None))
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
- sel_normed, _ = self._get_selection(selection)
2641
- elements = self.task.workflow.get_task_elements(self.task, sel_normed)
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
- def _get_selection(self, selection):
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
- start, stop, step = selection, selection + 1, 1
2663
+ lst = [selection]
2663
2664
 
2664
2665
  elif isinstance(selection, slice):
2665
- start, stop, step = selection.start, selection.stop, selection.step
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` or a `slice` "
2673
- f"object, but received type {type(selection)!r}."
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__(self, selection: Union[int, slice]) -> Union[Any, List[Any]]:
2686
- selection, length = self._get_selection(selection)
2687
- elements = self.task.workflow.get_task_elements(self.task, selection)
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 length == 1:
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