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.
- hpcflow/_version.py +1 -1
- hpcflow/sdk/__init__.py +2 -0
- hpcflow/sdk/app.py +194 -41
- 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 +22 -14
- 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_submission.py +1 -0
- hpcflow/tests/unit/test_utils.py +28 -0
- {hpcflow_new2-0.2.0a159.dist-info → hpcflow_new2-0.2.0a161.dist-info}/METADATA +1 -1
- {hpcflow_new2-0.2.0a159.dist-info → hpcflow_new2-0.2.0a161.dist-info}/RECORD +29 -28
- {hpcflow_new2-0.2.0a159.dist-info → hpcflow_new2-0.2.0a161.dist-info}/WHEEL +0 -0
- {hpcflow_new2-0.2.0a159.dist-info → hpcflow_new2-0.2.0a161.dist-info}/entry_points.txt +0 -0
hpcflow/sdk/core/actions.py
CHANGED
@@ -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(
|
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
|
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
|
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("{
|
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
|
-
|
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,
|
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("{
|
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
|
-
|
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
|
-
|
194
|
-
|
195
|
-
|
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,
|
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("{
|
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
|
-
|
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
|
-
|
347
|
-
|
348
|
-
|
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):
|
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:
|
@@ -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
@@ -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
|