hpcflow-new2 0.2.0a176__py3-none-any.whl → 0.2.0a178__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/cli.py +97 -4
- hpcflow/sdk/cli_common.py +22 -0
- hpcflow/sdk/core/cache.py +142 -0
- hpcflow/sdk/core/element.py +7 -0
- hpcflow/sdk/core/loop.py +105 -84
- hpcflow/sdk/core/loop_cache.py +140 -0
- hpcflow/sdk/core/task.py +29 -24
- hpcflow/sdk/core/utils.py +11 -1
- hpcflow/sdk/core/workflow.py +108 -24
- hpcflow/sdk/persistence/base.py +16 -3
- hpcflow/sdk/persistence/json.py +11 -4
- hpcflow/sdk/persistence/pending.py +2 -0
- hpcflow/sdk/persistence/zarr.py +132 -3
- hpcflow/tests/unit/test_persistence.py +118 -1
- hpcflow/tests/unit/test_utils.py +21 -0
- hpcflow_new2-0.2.0a178.dist-info/LICENSE +375 -0
- {hpcflow_new2-0.2.0a176.dist-info → hpcflow_new2-0.2.0a178.dist-info}/METADATA +1 -1
- {hpcflow_new2-0.2.0a176.dist-info → hpcflow_new2-0.2.0a178.dist-info}/RECORD +21 -18
- {hpcflow_new2-0.2.0a176.dist-info → hpcflow_new2-0.2.0a178.dist-info}/WHEEL +0 -0
- {hpcflow_new2-0.2.0a176.dist-info → hpcflow_new2-0.2.0a178.dist-info}/entry_points.txt +0 -0
hpcflow/sdk/core/loop.py
CHANGED
@@ -6,9 +6,11 @@ from typing import Dict, List, Optional, Tuple, Union
|
|
6
6
|
from hpcflow.sdk import app
|
7
7
|
from hpcflow.sdk.core.errors import LoopTaskSubsetError
|
8
8
|
from hpcflow.sdk.core.json_like import ChildObjectSpec, JSONLike
|
9
|
+
from hpcflow.sdk.core.loop_cache import LoopCache
|
9
10
|
from hpcflow.sdk.core.parameters import InputSourceType
|
10
11
|
from hpcflow.sdk.core.task import WorkflowTask
|
11
|
-
from hpcflow.sdk.core.utils import check_valid_py_identifier
|
12
|
+
from hpcflow.sdk.core.utils import check_valid_py_identifier, nth_key, nth_value
|
13
|
+
from hpcflow.sdk.log import TimeIt
|
12
14
|
|
13
15
|
# from .parameters import Parameter
|
14
16
|
|
@@ -198,6 +200,7 @@ class WorkflowLoop:
|
|
198
200
|
|
199
201
|
self._validate()
|
200
202
|
|
203
|
+
@TimeIt.decorator
|
201
204
|
def _validate(self):
|
202
205
|
# task subset must be a contiguous range of task indices:
|
203
206
|
task_indices = self.task_indices
|
@@ -328,6 +331,7 @@ class WorkflowLoop:
|
|
328
331
|
return self.workflow.tasks[: self.task_objects[0].index]
|
329
332
|
|
330
333
|
@staticmethod
|
334
|
+
@TimeIt.decorator
|
331
335
|
def _find_iterable_parameters(loop_template: app.Loop):
|
332
336
|
all_inputs_first_idx = {}
|
333
337
|
all_outputs_idx = {}
|
@@ -355,18 +359,19 @@ class WorkflowLoop:
|
|
355
359
|
return iterable_params
|
356
360
|
|
357
361
|
@classmethod
|
362
|
+
@TimeIt.decorator
|
358
363
|
def new_empty_loop(
|
359
364
|
cls,
|
360
365
|
index: int,
|
361
366
|
workflow: app.Workflow,
|
362
367
|
template: app.Loop,
|
363
|
-
|
368
|
+
iter_loop_idx: List[Dict],
|
364
369
|
) -> Tuple[app.WorkflowLoop, List[Dict[str, int]]]:
|
365
370
|
parent_loops = cls._get_parent_loops(index, workflow, template)
|
366
371
|
parent_names = [i.name for i in parent_loops]
|
367
372
|
num_added_iters = {}
|
368
|
-
for
|
369
|
-
num_added_iters[tuple([
|
373
|
+
for i in iter_loop_idx:
|
374
|
+
num_added_iters[tuple([i[j] for j in parent_names])] = 1
|
370
375
|
|
371
376
|
obj = cls(
|
372
377
|
index=index,
|
@@ -379,6 +384,7 @@ class WorkflowLoop:
|
|
379
384
|
return obj
|
380
385
|
|
381
386
|
@classmethod
|
387
|
+
@TimeIt.decorator
|
382
388
|
def _get_parent_loops(
|
383
389
|
cls,
|
384
390
|
index: int,
|
@@ -399,12 +405,14 @@ class WorkflowLoop:
|
|
399
405
|
parents.append(loop_i)
|
400
406
|
return parents
|
401
407
|
|
408
|
+
@TimeIt.decorator
|
402
409
|
def get_parent_loops(self) -> List[app.WorkflowLoop]:
|
403
410
|
"""Get loops whose task subset is a superset of this loop's task subset. If two
|
404
411
|
loops have identical task subsets, the first loop in the workflow loop list is
|
405
412
|
considered the child."""
|
406
413
|
return self._get_parent_loops(self.index, self.workflow, self.template)
|
407
414
|
|
415
|
+
@TimeIt.decorator
|
408
416
|
def get_child_loops(self) -> List[app.WorkflowLoop]:
|
409
417
|
"""Get loops whose task subset is a subset of this loop's task subset. If two
|
410
418
|
loops have identical task subsets, the first loop in the workflow loop list is
|
@@ -426,10 +434,12 @@ class WorkflowLoop:
|
|
426
434
|
children = sorted(children, key=lambda x: len(next(iter(x.num_added_iterations))))
|
427
435
|
return children
|
428
436
|
|
429
|
-
|
437
|
+
@TimeIt.decorator
|
438
|
+
def add_iteration(self, parent_loop_indices=None, cache: Optional[LoopCache] = None):
|
439
|
+
if not cache:
|
440
|
+
cache = LoopCache.build(self.workflow)
|
430
441
|
parent_loops = self.get_parent_loops()
|
431
442
|
child_loops = self.get_child_loops()
|
432
|
-
child_loop_names = [i.name for i in child_loops]
|
433
443
|
parent_loop_indices = parent_loop_indices or {}
|
434
444
|
if parent_loops and not parent_loop_indices:
|
435
445
|
parent_loop_indices = {i.name: 0 for i in parent_loops}
|
@@ -458,24 +468,19 @@ class WorkflowLoop:
|
|
458
468
|
if task.insert_ID in child.task_insert_IDs
|
459
469
|
},
|
460
470
|
}
|
471
|
+
added_iter_IDs = []
|
461
472
|
for elem_idx in range(task.num_elements):
|
462
|
-
|
463
|
-
|
464
|
-
|
473
|
+
|
474
|
+
elem_ID = task.element_IDs[elem_idx]
|
475
|
+
|
465
476
|
new_data_idx = {}
|
466
|
-
existing_inners = []
|
467
|
-
for iter_i in element.iterations:
|
468
|
-
if iter_i.loop_idx[self.name] == cur_loop_idx:
|
469
|
-
existing_inner_i = {
|
470
|
-
k: v
|
471
|
-
for k, v in iter_i.loop_idx.items()
|
472
|
-
if k in child_loop_names
|
473
|
-
}
|
474
|
-
if existing_inner_i:
|
475
|
-
existing_inners.append(existing_inner_i)
|
476
477
|
|
477
478
|
# copy resources from zeroth iteration:
|
478
|
-
|
479
|
+
zeroth_iter_ID, zi_iter_data_idx = cache.zeroth_iters[elem_ID]
|
480
|
+
zi_elem_ID, zi_idx = cache.iterations[zeroth_iter_ID]
|
481
|
+
zi_data_idx = nth_value(cache.data_idx[zi_elem_ID], zi_idx)
|
482
|
+
|
483
|
+
for key, val in zi_data_idx.items():
|
479
484
|
if key.startswith("resources."):
|
480
485
|
new_data_idx[key] = val
|
481
486
|
|
@@ -493,41 +498,47 @@ class WorkflowLoop:
|
|
493
498
|
# identify element(s) from which this iterable input should be
|
494
499
|
# parametrised:
|
495
500
|
if task.insert_ID == iter_dat["output_tasks"][-1]:
|
496
|
-
|
501
|
+
src_elem_ID = elem_ID
|
497
502
|
grouped_elems = None
|
498
503
|
else:
|
499
|
-
|
500
|
-
|
501
|
-
|
504
|
+
src_elem_IDs_all = cache.element_dependents[elem_ID]
|
505
|
+
src_elem_IDs = {
|
506
|
+
k: v
|
507
|
+
for k, v in src_elem_IDs_all.items()
|
508
|
+
if cache.elements[k]["task_insert_ID"]
|
509
|
+
== iter_dat["output_tasks"][-1]
|
510
|
+
}
|
502
511
|
# consider groups
|
503
512
|
inp_group_name = inp.single_labelled_data.get("group")
|
504
513
|
grouped_elems = []
|
505
|
-
for
|
514
|
+
for src_elem_j_ID, src_elem_j_dat in src_elem_IDs.items():
|
506
515
|
i_in_group = any(
|
507
|
-
|
516
|
+
k == inp_group_name
|
517
|
+
for k in src_elem_j_dat["group_names"]
|
508
518
|
)
|
509
519
|
if i_in_group:
|
510
|
-
grouped_elems.append(
|
520
|
+
grouped_elems.append(src_elem_j_ID)
|
511
521
|
|
512
|
-
if not grouped_elems and len(
|
522
|
+
if not grouped_elems and len(src_elem_IDs) > 1:
|
513
523
|
raise NotImplementedError(
|
514
|
-
f"Multiple elements found in the iterable parameter
|
515
|
-
f" latest output task (insert ID: "
|
516
|
-
f"{iter_dat['output_tasks'][-1]}) that can be used
|
517
|
-
f"parametrise the next iteration:
|
524
|
+
f"Multiple elements found in the iterable parameter "
|
525
|
+
f"{inp!r}'s latest output task (insert ID: "
|
526
|
+
f"{iter_dat['output_tasks'][-1]}) that can be used "
|
527
|
+
f"to parametrise the next iteration: "
|
528
|
+
f"{list(src_elem_IDs.keys())!r}."
|
518
529
|
)
|
519
530
|
|
520
|
-
elif not
|
531
|
+
elif not src_elem_IDs:
|
521
532
|
# TODO: maybe OK?
|
522
533
|
raise NotImplementedError(
|
523
|
-
f"No elements found in the iterable parameter
|
524
|
-
f" latest output task (insert ID: "
|
525
|
-
f"{iter_dat['output_tasks'][-1]}) that can be used
|
526
|
-
f"parametrise the next iteration."
|
534
|
+
f"No elements found in the iterable parameter "
|
535
|
+
f"{inp!r}'s latest output task (insert ID: "
|
536
|
+
f"{iter_dat['output_tasks'][-1]}) that can be used "
|
537
|
+
f"to parametrise the next iteration."
|
527
538
|
)
|
528
539
|
|
529
540
|
else:
|
530
|
-
|
541
|
+
src_elem_ID = nth_key(src_elem_IDs, 0)
|
531
542
|
|
532
543
|
child_loop_max_iters = {}
|
533
544
|
parent_loop_same_iters = {
|
@@ -553,76 +564,69 @@ class WorkflowLoop:
|
|
553
564
|
|
554
565
|
# identify the ElementIteration from which this input should be
|
555
566
|
# parametrised:
|
556
|
-
|
567
|
+
loop_idx_key = tuple(sorted(source_iter_loop_idx.items()))
|
557
568
|
if grouped_elems:
|
558
|
-
|
559
|
-
for
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
break
|
569
|
+
src_data_idx = []
|
570
|
+
for src_elem_ID in grouped_elems:
|
571
|
+
src_data_idx.append(
|
572
|
+
cache.data_idx[src_elem_ID][loop_idx_key]
|
573
|
+
)
|
564
574
|
else:
|
565
|
-
|
566
|
-
if iter_i.loop_idx == source_iter_loop_idx:
|
567
|
-
source_iter = iter_i
|
568
|
-
break
|
575
|
+
src_data_idx = cache.data_idx[src_elem_ID][loop_idx_key]
|
569
576
|
|
570
|
-
if not
|
577
|
+
if not src_data_idx:
|
571
578
|
raise RuntimeError(
|
572
579
|
f"Could not find a source iteration with loop_idx: "
|
573
580
|
f"{source_iter_loop_idx!r}."
|
574
581
|
)
|
575
582
|
|
576
583
|
if grouped_elems:
|
577
|
-
inp_dat_idx = [
|
578
|
-
i.get_data_idx()[f"outputs.{inp.typ}"]
|
579
|
-
for i in source_iter
|
580
|
-
]
|
584
|
+
inp_dat_idx = [i[f"outputs.{inp.typ}"] for i in src_data_idx]
|
581
585
|
else:
|
582
|
-
inp_dat_idx =
|
586
|
+
inp_dat_idx = src_data_idx[f"outputs.{inp.typ}"]
|
583
587
|
new_data_idx[f"inputs.{inp.typ}"] = inp_dat_idx
|
584
588
|
|
585
589
|
else:
|
586
590
|
inp_key = f"inputs.{inp.typ}"
|
587
591
|
|
588
|
-
orig_inp_src =
|
592
|
+
orig_inp_src = cache.elements[elem_ID]["input_sources"][inp_key]
|
589
593
|
inp_dat_idx = None
|
590
594
|
|
591
595
|
if orig_inp_src.source_type is InputSourceType.LOCAL:
|
592
596
|
# keep locally defined inputs from original element
|
593
|
-
inp_dat_idx =
|
597
|
+
inp_dat_idx = zi_data_idx[inp_key]
|
594
598
|
|
595
599
|
elif orig_inp_src.source_type is InputSourceType.DEFAULT:
|
596
600
|
# keep default value from original element
|
597
|
-
inp_dat_idx_iter_0 = element.iterations[0].get_data_idx()
|
598
601
|
try:
|
599
|
-
inp_dat_idx =
|
602
|
+
inp_dat_idx = zi_data_idx[inp_key]
|
600
603
|
except KeyError:
|
601
604
|
# if this input is required by a conditional action, and
|
602
605
|
# that condition is not met, then this input will not
|
603
606
|
# exist in the action-run data index, so use the initial
|
604
607
|
# iteration data index:
|
605
|
-
inp_dat_idx =
|
608
|
+
inp_dat_idx = zi_iter_data_idx[inp_key]
|
606
609
|
|
607
610
|
elif orig_inp_src.source_type is InputSourceType.TASK:
|
608
611
|
if orig_inp_src.task_ref not in self.task_insert_IDs:
|
609
|
-
# TODO: what about groups?
|
610
612
|
# source the data_idx from the iteration with same parent
|
611
613
|
# loop indices as the new iteration to add:
|
612
|
-
src_iters = []
|
613
|
-
|
614
|
+
# src_iters = []
|
615
|
+
src_data_idx = []
|
616
|
+
for li_k, di_k in cache.data_idx[elem_ID].items():
|
614
617
|
skip_iter = False
|
618
|
+
li_k_dct = dict(li_k)
|
615
619
|
for p_k, p_v in parent_loop_indices.items():
|
616
|
-
if
|
620
|
+
if li_k_dct.get(p_k) != p_v:
|
617
621
|
skip_iter = True
|
618
622
|
break
|
619
623
|
if not skip_iter:
|
620
|
-
|
624
|
+
src_data_idx.append(di_k)
|
621
625
|
|
622
626
|
# could be multiple, but they should all have the same
|
623
627
|
# data index for this parameter:
|
624
|
-
|
625
|
-
inp_dat_idx =
|
628
|
+
src_data_idx = src_data_idx[0]
|
629
|
+
inp_dat_idx = src_data_idx[inp_key]
|
626
630
|
else:
|
627
631
|
is_group = False
|
628
632
|
if (
|
@@ -645,19 +649,24 @@ class WorkflowLoop:
|
|
645
649
|
# find which element in that task `element`
|
646
650
|
# depends on:
|
647
651
|
task_i = self.workflow.tasks.get(insert_ID=tiID)
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
652
|
+
elem_i_ID = task_i.element_IDs[e_idx]
|
653
|
+
src_elem_IDs_all = cache.element_dependents[
|
654
|
+
elem_i_ID
|
655
|
+
]
|
656
|
+
src_elem_IDs_i = {
|
657
|
+
k: v
|
658
|
+
for k, v in src_elem_IDs_all.items()
|
659
|
+
if cache.elements[k]["task_insert_ID"]
|
660
|
+
== task.insert_ID
|
661
|
+
}
|
662
|
+
|
663
|
+
# filter src_elem_IDs_i for matching element IDs:
|
664
|
+
src_elem_IDs_i = [
|
665
|
+
i for i in src_elem_IDs_i if i == elem_ID
|
657
666
|
]
|
658
667
|
if (
|
659
|
-
len(
|
660
|
-
and
|
668
|
+
len(src_elem_IDs_i) == 1
|
669
|
+
and src_elem_IDs_i[0] == elem_ID
|
661
670
|
):
|
662
671
|
new_sources.append((tiID, e_idx))
|
663
672
|
|
@@ -680,10 +689,11 @@ class WorkflowLoop:
|
|
680
689
|
new_data_idx[inp_key] = inp_dat_idx
|
681
690
|
|
682
691
|
# add any locally defined sub-parameters:
|
692
|
+
inp_statuses = cache.elements[elem_ID]["input_statuses"]
|
683
693
|
inp_status_inps = set([f"inputs.{i}" for i in inp_statuses])
|
684
694
|
sub_params = inp_status_inps - set(new_data_idx.keys())
|
685
695
|
for sub_param_i in sub_params:
|
686
|
-
sub_param_data_idx_iter_0 =
|
696
|
+
sub_param_data_idx_iter_0 = zi_data_idx
|
687
697
|
try:
|
688
698
|
sub_param_data_idx = sub_param_data_idx_iter_0[sub_param_i]
|
689
699
|
except KeyError:
|
@@ -691,7 +701,7 @@ class WorkflowLoop:
|
|
691
701
|
# and that condition is not met, then this input will not exist in
|
692
702
|
# the action-run data index, so use the initial iteration data
|
693
703
|
# index:
|
694
|
-
sub_param_data_idx =
|
704
|
+
sub_param_data_idx = zi_data_idx[sub_param_i]
|
695
705
|
|
696
706
|
new_data_idx[sub_param_i] = sub_param_data_idx
|
697
707
|
|
@@ -703,16 +713,26 @@ class WorkflowLoop:
|
|
703
713
|
schema_params = set(
|
704
714
|
i for i in new_data_idx.keys() if len(i.split(".")) == 2
|
705
715
|
)
|
706
|
-
all_new_data_idx[(task.insert_ID,
|
716
|
+
all_new_data_idx[(task.insert_ID, elem_idx)] = new_data_idx
|
707
717
|
|
708
718
|
iter_ID_i = self.workflow._store.add_element_iteration(
|
709
|
-
element_ID=
|
719
|
+
element_ID=elem_ID,
|
710
720
|
data_idx=new_data_idx,
|
711
721
|
schema_parameters=list(schema_params),
|
712
722
|
loop_idx=new_loop_idx,
|
713
723
|
)
|
724
|
+
if cache:
|
725
|
+
cache.add_iteration(
|
726
|
+
iter_ID=iter_ID_i,
|
727
|
+
task_insert_ID=task.insert_ID,
|
728
|
+
element_ID=elem_ID,
|
729
|
+
loop_idx=new_loop_idx,
|
730
|
+
data_idx=new_data_idx,
|
731
|
+
)
|
714
732
|
|
715
|
-
|
733
|
+
added_iter_IDs.append(iter_ID_i)
|
734
|
+
|
735
|
+
task.initialise_EARs(iter_IDs=added_iter_IDs)
|
716
736
|
|
717
737
|
added_iters_key = tuple(parent_loop_indices[k] for k in self.parents)
|
718
738
|
self._increment_pending_added_iters(added_iters_key)
|
@@ -731,7 +751,8 @@ class WorkflowLoop:
|
|
731
751
|
**par_idx,
|
732
752
|
**parent_loop_indices,
|
733
753
|
self.name: cur_loop_idx + 1,
|
734
|
-
}
|
754
|
+
},
|
755
|
+
cache=cache,
|
735
756
|
)
|
736
757
|
|
737
758
|
def test_termination(self, element_iter):
|
@@ -0,0 +1,140 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from collections import defaultdict
|
3
|
+
from typing import Dict, List, Optional, Tuple
|
4
|
+
|
5
|
+
from hpcflow.sdk import app
|
6
|
+
from hpcflow.sdk.core.utils import nth_key
|
7
|
+
from hpcflow.sdk.log import TimeIt
|
8
|
+
from hpcflow.sdk.core.cache import DependencyCache
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass
|
12
|
+
class LoopCache:
|
13
|
+
"""Class to store a cache for use in `Workflow.add_empty_loop` and
|
14
|
+
`WorkflowLoop.add_iterations`.
|
15
|
+
|
16
|
+
Attributes
|
17
|
+
----------
|
18
|
+
element_dependents
|
19
|
+
Keys are element IDs, values are dicts whose keys are element IDs that depend on
|
20
|
+
the key element ID (via `Element.get_dependent_elements_recursively`), and whose
|
21
|
+
values are dicts with keys: `group_names`, which is a tuple of the string group
|
22
|
+
names associated with the dependent element's element set.
|
23
|
+
elements
|
24
|
+
Keys are element IDs, values are dicts with keys: `input_statuses`,
|
25
|
+
`input_sources`, and `task_insert_ID`.
|
26
|
+
zeroth_iters
|
27
|
+
Keys are element IDs, values are data associated with the zeroth iteration of that
|
28
|
+
element, namely a tuple of iteration ID and `ElementIteration.data_idx`.
|
29
|
+
data_idx
|
30
|
+
Keys are element IDs, values are data associated with all iterations of that
|
31
|
+
element, namely a dict whose keys are the iteration loop index as a tuple, and
|
32
|
+
whose values are data indices via `ElementIteration.get_data_idx()`.
|
33
|
+
iterations
|
34
|
+
Keys are iteration IDs, values are tuples of element ID and iteration index within
|
35
|
+
that element.
|
36
|
+
task_iterations
|
37
|
+
Keys are task insert IDs, values are list of all iteration IDs associated with
|
38
|
+
that task.
|
39
|
+
|
40
|
+
"""
|
41
|
+
|
42
|
+
element_dependents: Dict[int, Dict]
|
43
|
+
elements: Dict[int, Dict]
|
44
|
+
zeroth_iters: Dict[int, Tuple]
|
45
|
+
data_idx: Dict[int, Dict]
|
46
|
+
iterations: Dict[int, Tuple]
|
47
|
+
task_iterations: Dict[int, List[int]]
|
48
|
+
|
49
|
+
@TimeIt.decorator
|
50
|
+
def get_iter_IDs(self, loop: "app.Loop") -> List[int]:
|
51
|
+
"""Retrieve a list of iteration IDs belonging to a given loop."""
|
52
|
+
return [j for i in loop.task_insert_IDs for j in self.task_iterations[i]]
|
53
|
+
|
54
|
+
@TimeIt.decorator
|
55
|
+
def get_iter_loop_indices(self, iter_IDs: List[int]) -> List[Dict[str, int]]:
|
56
|
+
iter_loop_idx = []
|
57
|
+
for i in iter_IDs:
|
58
|
+
elem_id, idx = self.iterations[i]
|
59
|
+
iter_loop_idx.append(dict(nth_key(self.data_idx[elem_id], idx)))
|
60
|
+
return iter_loop_idx
|
61
|
+
|
62
|
+
@TimeIt.decorator
|
63
|
+
def update_loop_indices(self, new_loop_name: str, iter_IDs: List[int]):
|
64
|
+
elem_ids = {v[0] for k, v in self.iterations.items() if k in iter_IDs}
|
65
|
+
for i in elem_ids:
|
66
|
+
new_item = {}
|
67
|
+
for k, v in self.data_idx[i].items():
|
68
|
+
new_k = dict(k)
|
69
|
+
new_k.update({new_loop_name: 0})
|
70
|
+
new_item[tuple(sorted(new_k.items()))] = v
|
71
|
+
self.data_idx[i] = new_item
|
72
|
+
|
73
|
+
@TimeIt.decorator
|
74
|
+
def add_iteration(self, iter_ID, task_insert_ID, element_ID, loop_idx, data_idx):
|
75
|
+
"""Update the cache to include a newly added iteration."""
|
76
|
+
self.task_iterations[task_insert_ID].append(iter_ID)
|
77
|
+
new_iter_idx = len(self.data_idx[element_ID])
|
78
|
+
self.data_idx[element_ID][tuple(sorted(loop_idx.items()))] = data_idx
|
79
|
+
self.iterations[iter_ID] = (element_ID, new_iter_idx)
|
80
|
+
|
81
|
+
@classmethod
|
82
|
+
@TimeIt.decorator
|
83
|
+
def build(cls, workflow: "app.Workflow", loops: Optional[List["app.Loop"]] = None):
|
84
|
+
"""Build a cache of data for use in adding loops and iterations."""
|
85
|
+
|
86
|
+
deps_cache = DependencyCache.build(workflow)
|
87
|
+
|
88
|
+
loops = list(workflow.template.loops) + (loops or [])
|
89
|
+
task_iIDs = set(j for i in loops for j in i.task_insert_IDs)
|
90
|
+
tasks = [workflow.tasks.get(insert_ID=i) for i in sorted(task_iIDs)]
|
91
|
+
elem_deps = {}
|
92
|
+
|
93
|
+
# keys: element IDs, values: dict with keys: tuple(loop_idx), values: data index
|
94
|
+
data_idx_cache = {}
|
95
|
+
|
96
|
+
# keys: iteration IDs, values: tuple of (element ID, integer index into values
|
97
|
+
# dict in `data_idx_cache` [accessed via `.keys()[index]`])
|
98
|
+
iters = {}
|
99
|
+
|
100
|
+
# keys: element IDs, values: dict with keys: "input_statues", "input_sources",
|
101
|
+
# "task_insert_ID":
|
102
|
+
elements = {}
|
103
|
+
|
104
|
+
zeroth_iters = {}
|
105
|
+
task_iterations = defaultdict(list)
|
106
|
+
for task in tasks:
|
107
|
+
for elem_idx in task.element_IDs:
|
108
|
+
element = deps_cache.elements[elem_idx]
|
109
|
+
inp_statuses = task.template.get_input_statuses(element.element_set)
|
110
|
+
elements[element.id_] = {
|
111
|
+
"input_statuses": inp_statuses,
|
112
|
+
"input_sources": element.input_sources,
|
113
|
+
"task_insert_ID": task.insert_ID,
|
114
|
+
}
|
115
|
+
elem_deps[element.id_] = {
|
116
|
+
i: {
|
117
|
+
"group_names": tuple(
|
118
|
+
j.name for j in deps_cache.elements[i].element_set.groups
|
119
|
+
),
|
120
|
+
}
|
121
|
+
for i in deps_cache.elem_elem_dependents_rec[element.id_]
|
122
|
+
}
|
123
|
+
elem_iters = {}
|
124
|
+
for idx, iter_i in enumerate(element.iterations):
|
125
|
+
if idx == 0:
|
126
|
+
zeroth_iters[element.id_] = (iter_i.id_, iter_i.data_idx)
|
127
|
+
loop_idx_key = tuple(sorted(iter_i.loop_idx.items()))
|
128
|
+
elem_iters[loop_idx_key] = iter_i.get_data_idx()
|
129
|
+
task_iterations[task.insert_ID].append(iter_i.id_)
|
130
|
+
iters[iter_i.id_] = (element.id_, idx)
|
131
|
+
data_idx_cache[element.id_] = elem_iters
|
132
|
+
|
133
|
+
return cls(
|
134
|
+
element_dependents=elem_deps,
|
135
|
+
elements=elements,
|
136
|
+
zeroth_iters=zeroth_iters,
|
137
|
+
data_idx=data_idx_cache,
|
138
|
+
iterations=iters,
|
139
|
+
task_iterations=dict(task_iterations),
|
140
|
+
)
|
hpcflow/sdk/core/task.py
CHANGED
@@ -2062,29 +2062,36 @@ class WorkflowTask:
|
|
2062
2062
|
return element_dat_idx
|
2063
2063
|
|
2064
2064
|
@TimeIt.decorator
|
2065
|
-
def initialise_EARs(self) -> List[int]:
|
2065
|
+
def initialise_EARs(self, iter_IDs: Optional[List[int]] = None) -> List[int]:
|
2066
2066
|
"""Try to initialise any uninitialised EARs of this task."""
|
2067
|
+
if iter_IDs:
|
2068
|
+
iters = self.workflow.get_element_iterations_from_IDs(iter_IDs)
|
2069
|
+
else:
|
2070
|
+
iters = []
|
2071
|
+
for element in self.elements:
|
2072
|
+
# We don't yet cache Element objects, so `element`, and also it's
|
2073
|
+
# `ElementIterations, are transient. So there is no reason to update these
|
2074
|
+
# objects in memory to account for the new EARs. Subsequent calls to
|
2075
|
+
# `WorkflowTask.elements` will retrieve correct element data from the
|
2076
|
+
# store. This might need changing once/if we start caching Element
|
2077
|
+
# objects.
|
2078
|
+
iters.extend(element.iterations)
|
2079
|
+
|
2067
2080
|
initialised = []
|
2068
|
-
for
|
2069
|
-
|
2070
|
-
|
2071
|
-
|
2072
|
-
|
2073
|
-
|
2074
|
-
|
2075
|
-
|
2076
|
-
|
2077
|
-
|
2078
|
-
|
2079
|
-
|
2080
|
-
|
2081
|
-
|
2082
|
-
f"UnsetParameterDataError raised: cannot yet initialise runs."
|
2083
|
-
)
|
2084
|
-
pass
|
2085
|
-
else:
|
2086
|
-
iter_i._EARs_initialised = True
|
2087
|
-
self.workflow.set_EARs_initialised(iter_i.id_)
|
2081
|
+
for iter_i in iters:
|
2082
|
+
if not iter_i.EARs_initialised:
|
2083
|
+
try:
|
2084
|
+
self._initialise_element_iter_EARs(iter_i)
|
2085
|
+
initialised.append(iter_i.id_)
|
2086
|
+
except UnsetParameterDataError:
|
2087
|
+
# raised by `Action.test_rules`; cannot yet initialise EARs
|
2088
|
+
self.app.logger.debug(
|
2089
|
+
f"UnsetParameterDataError raised: cannot yet initialise runs."
|
2090
|
+
)
|
2091
|
+
pass
|
2092
|
+
else:
|
2093
|
+
iter_i._EARs_initialised = True
|
2094
|
+
self.workflow.set_EARs_initialised(iter_i.id_)
|
2088
2095
|
return initialised
|
2089
2096
|
|
2090
2097
|
@TimeIt.decorator
|
@@ -2097,7 +2104,6 @@ class WorkflowTask:
|
|
2097
2104
|
param_src_updates = {}
|
2098
2105
|
|
2099
2106
|
count = 0
|
2100
|
-
# TODO: generator is an IO op here, can be pre-calculated/cached?
|
2101
2107
|
for act_idx, action in self.template.all_schema_actions():
|
2102
2108
|
log_common = (
|
2103
2109
|
f"for action {act_idx} of element iteration {element_iter.index} of "
|
@@ -2151,8 +2157,7 @@ class WorkflowTask:
|
|
2151
2157
|
metadata={},
|
2152
2158
|
)
|
2153
2159
|
|
2154
|
-
|
2155
|
-
self.workflow._store.update_param_source(pid, src)
|
2160
|
+
self.workflow._store.update_param_source(param_src_updates)
|
2156
2161
|
|
2157
2162
|
@TimeIt.decorator
|
2158
2163
|
def _add_element_set(self, element_set):
|
hpcflow/sdk/core/utils.py
CHANGED
@@ -3,7 +3,7 @@ import enum
|
|
3
3
|
from functools import wraps
|
4
4
|
import contextlib
|
5
5
|
import hashlib
|
6
|
-
from itertools import accumulate
|
6
|
+
from itertools import accumulate, islice
|
7
7
|
import json
|
8
8
|
import keyword
|
9
9
|
import os
|
@@ -871,3 +871,13 @@ def dict_values_process_flat(d, callable):
|
|
871
871
|
out[k] = proc_idx_k
|
872
872
|
|
873
873
|
return out
|
874
|
+
|
875
|
+
|
876
|
+
def nth_key(dct, n):
|
877
|
+
it = iter(dct)
|
878
|
+
next(islice(it, n, n), None)
|
879
|
+
return next(it)
|
880
|
+
|
881
|
+
|
882
|
+
def nth_value(dct, n):
|
883
|
+
return dct[nth_key(dct, n)]
|