hpcflow-new2 0.2.0a159__py3-none-any.whl → 0.2.0a161__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.
@@ -29,6 +29,7 @@ from hpcflow.sdk.core.utils import (
29
29
  swap_nested_dict_keys,
30
30
  )
31
31
  from hpcflow.sdk.log import TimeIt
32
+ from hpcflow.sdk.core.run_dir_files import RunDirAppFiles
32
33
 
33
34
 
34
35
  ACTION_SCOPE_REGEX = r"(\w*)(?:\[(.*)\])?"
@@ -251,13 +252,16 @@ class ElementActionRun:
251
252
  @property
252
253
  def snapshot_start(self):
253
254
  if self._ss_start_obj is None and self._snapshot_start:
254
- self._ss_start_obj = JSONLikeDirSnapShot(**self._snapshot_start)
255
+ self._ss_start_obj = JSONLikeDirSnapShot(
256
+ root_path=".",
257
+ **self._snapshot_start,
258
+ )
255
259
  return self._ss_start_obj
256
260
 
257
261
  @property
258
262
  def snapshot_end(self):
259
263
  if self._ss_end_obj is None and self._snapshot_end:
260
- self._ss_end_obj = JSONLikeDirSnapShot(**self._snapshot_end)
264
+ self._ss_end_obj = JSONLikeDirSnapShot(root_path=".", **self._snapshot_end)
261
265
  return self._ss_end_obj
262
266
 
263
267
  @property
@@ -331,6 +335,7 @@ class ElementActionRun:
331
335
  run_idx=self.index,
332
336
  )
333
337
 
338
+ @TimeIt.decorator
334
339
  def get_parameter_sources(
335
340
  self,
336
341
  path: str = None,
@@ -363,6 +368,7 @@ class ElementActionRun:
363
368
  raise_on_unset=raise_on_unset,
364
369
  )
365
370
 
371
+ @TimeIt.decorator
366
372
  def get_EAR_dependencies(self, as_objects=False):
367
373
  """Get EARs that this EAR depends on."""
368
374
 
@@ -434,6 +440,7 @@ class ElementActionRun:
434
440
  return self._outputs
435
441
 
436
442
  @property
443
+ @TimeIt.decorator
437
444
  def resources(self):
438
445
  if not self._resources:
439
446
  self._resources = self.app.ElementResources(**self.get_resources())
@@ -451,6 +458,7 @@ class ElementActionRun:
451
458
  self._output_files = self.app.ElementOutputFiles(element_action_run=self)
452
459
  return self._output_files
453
460
 
461
+ @TimeIt.decorator
454
462
  def get_resources(self):
455
463
  """Resolve specific resources for this EAR, considering all applicable scopes and
456
464
  template-level resources."""
@@ -1457,11 +1465,11 @@ class Action(JSONLike):
1457
1465
 
1458
1466
  @staticmethod
1459
1467
  def get_param_dump_file_stem(js_idx: int, js_act_idx: int):
1460
- return f"js_{js_idx}_act_{js_act_idx}_inputs"
1468
+ return RunDirAppFiles.get_run_param_dump_file_prefix(js_idx, js_act_idx)
1461
1469
 
1462
1470
  @staticmethod
1463
1471
  def get_param_load_file_stem(js_idx: int, js_act_idx: int):
1464
- return f"js_{js_idx}_act_{js_act_idx}_outputs"
1472
+ return RunDirAppFiles.get_run_param_load_file_prefix(js_idx, js_act_idx)
1465
1473
 
1466
1474
  def get_param_dump_file_path_JSON(self, js_idx: int, js_act_idx: int):
1467
1475
  return Path(self.get_param_dump_file_stem(js_idx, js_act_idx) + ".json")
@@ -1945,7 +1953,7 @@ class Action(JSONLike):
1945
1953
  """\
1946
1954
  import {app_module} as app
1947
1955
  app.load_config(
1948
- log_file_path=Path("{app_package_name}.log").resolve(),
1956
+ log_file_path=Path("{run_log_file}").resolve(),
1949
1957
  config_dir=r"{cfg_dir}",
1950
1958
  config_key=r"{cfg_invoc_key}",
1951
1959
  )
@@ -1954,7 +1962,7 @@ class Action(JSONLike):
1954
1962
  EAR = wk.get_EARs_from_IDs([EAR_ID])[0]
1955
1963
  """
1956
1964
  ).format(
1957
- app_package_name=self.app.package_name,
1965
+ run_log_file=self.app.RunDirAppFiles.get_log_file_name(),
1958
1966
  app_module=self.app.module,
1959
1967
  cfg_dir=self.app.config.config_directory,
1960
1968
  cfg_invoc_key=self.app.config.config_key,
@@ -144,10 +144,9 @@ class InputFileGenerator(JSONLike):
144
144
  self.app.ActionRule.check_missing(f"input_files.{self.input_file.label}")
145
145
  ] + self.rules
146
146
 
147
- def compose_source(self, action) -> str:
147
+ def compose_source(self, snip_path) -> str:
148
148
  """Generate the file contents of this input file generator source."""
149
149
 
150
- snip_path = action.get_snippet_script_path(self.script)
151
150
  script_main_func = snip_path.stem
152
151
  with snip_path.open("rt") as fp:
153
152
  script_str = fp.read()
@@ -159,7 +158,7 @@ class InputFileGenerator(JSONLike):
159
158
  from pathlib import Path
160
159
  import {app_module} as app
161
160
  app.load_config(
162
- log_file_path=Path("{app_package_name}.log").resolve(),
161
+ log_file_path=Path("{run_log_file}").resolve(),
163
162
  config_dir=r"{cfg_dir}",
164
163
  config_key=r"{cfg_invoc_key}",
165
164
  )
@@ -171,7 +170,7 @@ class InputFileGenerator(JSONLike):
171
170
  """
172
171
  )
173
172
  main_block = main_block.format(
174
- app_package_name=self.app.package_name,
173
+ run_log_file=self.app.RunDirAppFiles.get_log_file_name(),
175
174
  app_module=self.app.module,
176
175
  cfg_dir=self.app.config.config_directory,
177
176
  cfg_invoc_key=self.app.config.config_key,
@@ -190,9 +189,14 @@ class InputFileGenerator(JSONLike):
190
189
  return out
191
190
 
192
191
  def write_source(self, action):
193
- script_path = action.get_script_name(self.script)
194
- with Path(script_path).open("wt", newline="\n") as fp:
195
- fp.write(self.compose_source(action))
192
+
193
+ # write the script if it is specified as a snippet script, otherwise we assume
194
+ # the script already exists in the working directory:
195
+ snip_path = action.get_snippet_script_path(self.script)
196
+ if snip_path:
197
+ source_str = self.compose_source(snip_path)
198
+ with Path(snip_path.name).open("wt", newline="\n") as fp:
199
+ fp.write(source_str)
196
200
 
197
201
 
198
202
  @dataclass
@@ -284,14 +288,13 @@ class OutputFileParser(JSONLike):
284
288
  for i in self.output_files
285
289
  ] + self.rules
286
290
 
287
- def compose_source(self, action) -> str:
291
+ def compose_source(self, snip_path) -> str:
288
292
  """Generate the file contents of this output file parser source."""
289
293
 
290
294
  if self.output is None:
291
295
  # might be used just for saving files:
292
296
  return
293
297
 
294
- snip_path = action.get_snippet_script_path(self.script)
295
298
  script_main_func = snip_path.stem
296
299
  with snip_path.open("rt") as fp:
297
300
  script_str = fp.read()
@@ -303,7 +306,7 @@ class OutputFileParser(JSONLike):
303
306
  from pathlib import Path
304
307
  import {app_module} as app
305
308
  app.load_config(
306
- log_file_path=Path("{app_package_name}.log").resolve(),
309
+ log_file_path=Path("{run_log_file}").resolve(),
307
310
  config_dir=r"{cfg_dir}",
308
311
  config_key=r"{cfg_invoc_key}",
309
312
  )
@@ -321,7 +324,7 @@ class OutputFileParser(JSONLike):
321
324
  """
322
325
  )
323
326
  main_block = main_block.format(
324
- app_package_name=self.app.package_name,
327
+ run_log_file=self.app.RunDirAppFiles.get_log_file_name(),
325
328
  app_module=self.app.module,
326
329
  cfg_dir=self.app.config.config_directory,
327
330
  cfg_invoc_key=self.app.config.config_key,
@@ -343,9 +346,14 @@ class OutputFileParser(JSONLike):
343
346
  if self.output is None:
344
347
  # might be used just for saving files:
345
348
  return
346
- script_path = action.get_script_name(self.script)
347
- with Path(script_path).open("wt", newline="\n") as fp:
348
- fp.write(self.compose_source(action))
349
+
350
+ # write the script if it is specified as a snippet script, otherwise we assume
351
+ # the script already exists in the working directory:
352
+ snip_path = action.get_snippet_script_path(self.script)
353
+ if snip_path:
354
+ source_str = self.compose_source(snip_path)
355
+ with Path(snip_path.name).open("wt", newline="\n") as fp:
356
+ fp.write(source_str)
349
357
 
350
358
 
351
359
  class _FileContentsSpecifier(JSONLike):
@@ -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:
@@ -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
@@ -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