hpcflow-new2 0.2.0a190__py3-none-any.whl → 0.2.0a199__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/__pyinstaller/hook-hpcflow.py +1 -0
- hpcflow/_version.py +1 -1
- hpcflow/data/scripts/bad_script.py +2 -0
- hpcflow/data/scripts/do_nothing.py +2 -0
- hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
- hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
- hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
- hpcflow/data/scripts/input_file_generator_basic.py +3 -0
- hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
- hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
- hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
- hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
- hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
- hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
- hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
- hpcflow/data/scripts/output_file_parser_basic.py +3 -0
- hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
- hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
- hpcflow/data/scripts/script_exit_test.py +5 -0
- hpcflow/data/template_components/environments.yaml +1 -1
- hpcflow/sdk/__init__.py +5 -0
- hpcflow/sdk/app.py +150 -89
- hpcflow/sdk/cli.py +263 -84
- hpcflow/sdk/cli_common.py +99 -5
- hpcflow/sdk/config/callbacks.py +38 -1
- hpcflow/sdk/config/config.py +102 -13
- hpcflow/sdk/config/errors.py +19 -5
- hpcflow/sdk/config/types.py +3 -0
- hpcflow/sdk/core/__init__.py +25 -1
- hpcflow/sdk/core/actions.py +914 -262
- hpcflow/sdk/core/cache.py +76 -34
- hpcflow/sdk/core/command_files.py +14 -128
- hpcflow/sdk/core/commands.py +35 -6
- hpcflow/sdk/core/element.py +122 -50
- hpcflow/sdk/core/errors.py +58 -2
- hpcflow/sdk/core/execute.py +207 -0
- hpcflow/sdk/core/loop.py +408 -50
- hpcflow/sdk/core/loop_cache.py +4 -4
- hpcflow/sdk/core/parameters.py +382 -37
- hpcflow/sdk/core/run_dir_files.py +13 -40
- hpcflow/sdk/core/skip_reason.py +7 -0
- hpcflow/sdk/core/task.py +119 -30
- hpcflow/sdk/core/task_schema.py +68 -0
- hpcflow/sdk/core/test_utils.py +66 -27
- hpcflow/sdk/core/types.py +54 -1
- hpcflow/sdk/core/utils.py +78 -7
- hpcflow/sdk/core/workflow.py +1538 -336
- hpcflow/sdk/data/workflow_spec_schema.yaml +2 -0
- hpcflow/sdk/demo/cli.py +7 -0
- hpcflow/sdk/helper/cli.py +1 -0
- hpcflow/sdk/log.py +42 -15
- hpcflow/sdk/persistence/base.py +405 -53
- hpcflow/sdk/persistence/json.py +177 -52
- hpcflow/sdk/persistence/pending.py +237 -69
- hpcflow/sdk/persistence/store_resource.py +3 -2
- hpcflow/sdk/persistence/types.py +15 -4
- hpcflow/sdk/persistence/zarr.py +928 -81
- hpcflow/sdk/submission/jobscript.py +1408 -489
- hpcflow/sdk/submission/schedulers/__init__.py +40 -5
- hpcflow/sdk/submission/schedulers/direct.py +33 -19
- hpcflow/sdk/submission/schedulers/sge.py +51 -16
- hpcflow/sdk/submission/schedulers/slurm.py +44 -16
- hpcflow/sdk/submission/schedulers/utils.py +7 -2
- hpcflow/sdk/submission/shells/base.py +68 -20
- hpcflow/sdk/submission/shells/bash.py +222 -129
- hpcflow/sdk/submission/shells/powershell.py +200 -150
- hpcflow/sdk/submission/submission.py +852 -119
- hpcflow/sdk/submission/types.py +18 -21
- hpcflow/sdk/typing.py +24 -5
- hpcflow/sdk/utils/arrays.py +71 -0
- hpcflow/sdk/utils/deferred_file.py +55 -0
- hpcflow/sdk/utils/hashing.py +16 -0
- hpcflow/sdk/utils/patches.py +12 -0
- hpcflow/sdk/utils/strings.py +33 -0
- hpcflow/tests/api/test_api.py +32 -0
- hpcflow/tests/conftest.py +19 -0
- hpcflow/tests/data/multi_path_sequences.yaml +29 -0
- hpcflow/tests/data/workflow_test_run_abort.yaml +34 -35
- hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
- hpcflow/tests/scripts/test_input_file_generators.py +282 -0
- hpcflow/tests/scripts/test_main_scripts.py +821 -70
- hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
- hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
- hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -0
- hpcflow/tests/unit/test_action.py +176 -0
- hpcflow/tests/unit/test_app.py +20 -0
- hpcflow/tests/unit/test_cache.py +46 -0
- hpcflow/tests/unit/test_cli.py +133 -0
- hpcflow/tests/unit/test_config.py +122 -1
- hpcflow/tests/unit/test_element_iteration.py +47 -0
- hpcflow/tests/unit/test_jobscript_unit.py +757 -0
- hpcflow/tests/unit/test_loop.py +1332 -27
- hpcflow/tests/unit/test_meta_task.py +325 -0
- hpcflow/tests/unit/test_multi_path_sequences.py +229 -0
- hpcflow/tests/unit/test_parameter.py +13 -0
- hpcflow/tests/unit/test_persistence.py +190 -8
- hpcflow/tests/unit/test_run.py +109 -3
- hpcflow/tests/unit/test_run_directories.py +29 -0
- hpcflow/tests/unit/test_shell.py +20 -0
- hpcflow/tests/unit/test_submission.py +5 -76
- hpcflow/tests/unit/utils/test_arrays.py +40 -0
- hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
- hpcflow/tests/unit/utils/test_hashing.py +65 -0
- hpcflow/tests/unit/utils/test_patches.py +5 -0
- hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
- hpcflow/tests/workflows/__init__.py +0 -0
- hpcflow/tests/workflows/test_directory_structure.py +31 -0
- hpcflow/tests/workflows/test_jobscript.py +332 -0
- hpcflow/tests/workflows/test_run_status.py +198 -0
- hpcflow/tests/workflows/test_skip_downstream.py +696 -0
- hpcflow/tests/workflows/test_submission.py +140 -0
- hpcflow/tests/workflows/test_workflows.py +142 -2
- hpcflow/tests/workflows/test_zip.py +18 -0
- hpcflow/viz_demo.ipynb +6587 -3
- {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a199.dist-info}/METADATA +7 -4
- hpcflow_new2-0.2.0a199.dist-info/RECORD +221 -0
- hpcflow_new2-0.2.0a190.dist-info/RECORD +0 -165
- {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a199.dist-info}/LICENSE +0 -0
- {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a199.dist-info}/WHEEL +0 -0
- {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a199.dist-info}/entry_points.txt +0 -0
hpcflow/sdk/core/loop_cache.py
CHANGED
@@ -10,7 +10,7 @@ from typing_extensions import Generic, TypeVar
|
|
10
10
|
|
11
11
|
from hpcflow.sdk.core.utils import nth_key
|
12
12
|
from hpcflow.sdk.log import TimeIt
|
13
|
-
from hpcflow.sdk.core.cache import
|
13
|
+
from hpcflow.sdk.core.cache import ObjectCache
|
14
14
|
|
15
15
|
if TYPE_CHECKING:
|
16
16
|
from collections.abc import Mapping, Sequence
|
@@ -222,7 +222,7 @@ class LoopCache:
|
|
222
222
|
def build(cls, workflow: Workflow, loops: list[Loop] | None = None) -> Self:
|
223
223
|
"""Build a cache of data for use in adding loops and iterations."""
|
224
224
|
|
225
|
-
deps_cache =
|
225
|
+
deps_cache = ObjectCache.build(workflow, dependencies=True, elements=True)
|
226
226
|
|
227
227
|
loops = [*workflow.template.loops, *(loops or ())]
|
228
228
|
task_iIDs = {t_id for loop in loops for t_id in loop.task_insert_IDs}
|
@@ -245,8 +245,8 @@ class LoopCache:
|
|
245
245
|
zeroth_iters: dict[int, tuple[int, DataIndex]] = {}
|
246
246
|
task_iterations = defaultdict(list)
|
247
247
|
for task in tasks:
|
248
|
-
for
|
249
|
-
element = deps_cache.elements[
|
248
|
+
for elem_id in task.element_IDs:
|
249
|
+
element = deps_cache.elements[elem_id]
|
250
250
|
inp_statuses = task.template.get_input_statuses(element.element_set)
|
251
251
|
elements[element.id_] = {
|
252
252
|
"input_statuses": inp_statuses,
|
hpcflow/sdk/core/parameters.py
CHANGED
@@ -14,6 +14,7 @@ from typing import TypeVar, cast, TYPE_CHECKING
|
|
14
14
|
from typing_extensions import override, TypeIs
|
15
15
|
|
16
16
|
import numpy as np
|
17
|
+
from scipy.stats.qmc import LatinHypercube
|
17
18
|
from valida import Schema as ValidaSchema # type: ignore
|
18
19
|
|
19
20
|
from hpcflow.sdk.typing import hydrate
|
@@ -43,6 +44,7 @@ if TYPE_CHECKING:
|
|
43
44
|
from typing import Any, ClassVar, Literal
|
44
45
|
from typing_extensions import Self, TypeAlias
|
45
46
|
from h5py import Group # type: ignore
|
47
|
+
from numpy.typing import NDArray
|
46
48
|
from ..app import BaseApp
|
47
49
|
from ..typing import ParamSource
|
48
50
|
from .actions import ActionScope
|
@@ -120,6 +122,13 @@ class ParameterValue:
|
|
120
122
|
"""
|
121
123
|
raise NotImplementedError
|
122
124
|
|
125
|
+
@classmethod
|
126
|
+
def dump_element_group_to_HDF5_group(cls, objs: list[ParameterValue], group: Group):
|
127
|
+
"""
|
128
|
+
Write a list (from an element group) of parameter values to an HDF5 group.
|
129
|
+
"""
|
130
|
+
raise NotImplementedError
|
131
|
+
|
123
132
|
@classmethod
|
124
133
|
def save_from_HDF5_group(cls, group: Group, param_id: int, workflow: Workflow):
|
125
134
|
"""
|
@@ -376,6 +385,13 @@ class SchemaInput(SchemaParameter):
|
|
376
385
|
Determines the name of the element group from which this input should be sourced.
|
377
386
|
This is a default value that will be applied to all `labels` if a "group" key
|
378
387
|
does not exist.
|
388
|
+
allow_failed_dependencies
|
389
|
+
This controls whether failure to retrieve inputs (i.e. an
|
390
|
+
`UnsetParameterDataError` is raised for one of the input sources) should be
|
391
|
+
allowed. By default, the unset value, which is equivalent to `False`, means no
|
392
|
+
failures are allowed. If set to `True`, any number of failures are allowed. If an
|
393
|
+
integer is specified, that number of failures are permitted. Finally, if a float
|
394
|
+
is specified, that proportion of failures are allowed.
|
379
395
|
"""
|
380
396
|
|
381
397
|
_task_schema: TaskSchema | None = None # assigned by parent TaskSchema
|
@@ -397,6 +413,7 @@ class SchemaInput(SchemaParameter):
|
|
397
413
|
default_value: InputValue | Any | NullDefault = NullDefault.NULL,
|
398
414
|
propagation_mode: ParameterPropagationMode = ParameterPropagationMode.IMPLICIT,
|
399
415
|
group: str | None = None,
|
416
|
+
allow_failed_dependencies: int | float | bool | None = False,
|
400
417
|
):
|
401
418
|
# TODO: can we define elements groups on local inputs as well, or should these be
|
402
419
|
# just for elements from other tasks?
|
@@ -413,8 +430,14 @@ class SchemaInput(SchemaParameter):
|
|
413
430
|
else:
|
414
431
|
self.parameter = parameter
|
415
432
|
|
416
|
-
|
433
|
+
if allow_failed_dependencies is None:
|
434
|
+
allow_failed_dependencies = 0.0
|
435
|
+
elif isinstance(allow_failed_dependencies, bool):
|
436
|
+
allow_failed_dependencies = float(allow_failed_dependencies)
|
437
|
+
|
438
|
+
#: Whether to expect multiple labels for this parameter.
|
417
439
|
self.multiple = multiple
|
440
|
+
self.allow_failed_dependencies = allow_failed_dependencies
|
418
441
|
|
419
442
|
#: Dict whose keys represent the string labels that distinguish multiple
|
420
443
|
#: parameters if `multiple` is `True`.
|
@@ -533,6 +556,7 @@ class SchemaInput(SchemaParameter):
|
|
533
556
|
"parameter": copy.deepcopy(self.parameter, memo),
|
534
557
|
"multiple": self.multiple,
|
535
558
|
"labels": copy.deepcopy(self.labels, memo),
|
559
|
+
"allow_failed_dependencies": self.allow_failed_dependencies,
|
536
560
|
}
|
537
561
|
obj = self.__class__(**kwargs)
|
538
562
|
obj._task_schema = self._task_schema
|
@@ -698,7 +722,52 @@ class BuiltinSchemaParameter:
|
|
698
722
|
pass
|
699
723
|
|
700
724
|
|
701
|
-
class
|
725
|
+
class _BaseSequence(JSONLike):
|
726
|
+
"""
|
727
|
+
A base class for shared methods of `ValueSequence` and `MultiPathSequence`.
|
728
|
+
"""
|
729
|
+
|
730
|
+
def __eq__(self, other: Any) -> bool:
|
731
|
+
if not isinstance(other, self.__class__):
|
732
|
+
return False
|
733
|
+
return self.to_dict() == other.to_dict()
|
734
|
+
|
735
|
+
@classmethod
|
736
|
+
def from_json_like(cls, json_like, shared_data=None):
|
737
|
+
if "path" in json_like: # note: singular
|
738
|
+
# only applicable to ValueSequence, although not well-defined/useful anyway,
|
739
|
+
# I think.
|
740
|
+
if "::" in json_like["path"]:
|
741
|
+
path, cls_method = json_like["path"].split("::")
|
742
|
+
json_like["path"] = path
|
743
|
+
json_like["value_class_method"] = cls_method
|
744
|
+
|
745
|
+
val_key = next((item for item in json_like if "values" in item), "")
|
746
|
+
if "::" in val_key:
|
747
|
+
# class method (e.g. `from_range`, `from_file` etc):
|
748
|
+
_, method = val_key.split("::")
|
749
|
+
_values_method_args = json_like.pop(val_key)
|
750
|
+
|
751
|
+
if "paths" in json_like: # note: plural
|
752
|
+
# only applicable to `MultiPathSequence`, where it is useful to know
|
753
|
+
# how many paths we are generating sequences for:
|
754
|
+
_values_method_args["paths"] = json_like["paths"]
|
755
|
+
|
756
|
+
_values_method = f"_values_{method}"
|
757
|
+
_values_method_args = _process_demo_data_strings(
|
758
|
+
cls._app, _values_method_args
|
759
|
+
)
|
760
|
+
json_like["values"] = getattr(cls, _values_method)(**_values_method_args)
|
761
|
+
|
762
|
+
obj = super().from_json_like(json_like, shared_data)
|
763
|
+
if "::" in val_key:
|
764
|
+
obj._values_method = method
|
765
|
+
obj._values_method_args = _values_method_args
|
766
|
+
|
767
|
+
return obj
|
768
|
+
|
769
|
+
|
770
|
+
class ValueSequence(_BaseSequence):
|
702
771
|
"""
|
703
772
|
A sequence of values.
|
704
773
|
|
@@ -719,7 +788,7 @@ class ValueSequence(JSONLike):
|
|
719
788
|
def __init__(
|
720
789
|
self,
|
721
790
|
path: str,
|
722
|
-
values:
|
791
|
+
values: Sequence[Any] | None,
|
723
792
|
nesting_order: int | float | None = None,
|
724
793
|
label: str | int | None = None,
|
725
794
|
value_class_method: str | None = None,
|
@@ -746,8 +815,8 @@ class ValueSequence(JSONLike):
|
|
746
815
|
bool
|
747
816
|
] | None = None # assigned initially on `make_persistent`
|
748
817
|
|
749
|
-
self._workflow: Workflow | None = None
|
750
|
-
self._element_set: ElementSet | None = None # assigned by parent ElementSet
|
818
|
+
self._workflow: Workflow | None = None # assigned in `make_persistent`
|
819
|
+
self._element_set: ElementSet | None = None # assigned by parent `ElementSet`
|
751
820
|
|
752
821
|
# assigned if this is an "inputs" sequence in `WorkflowTask._add_element_set`:
|
753
822
|
self._parameter: Parameter | None = None
|
@@ -776,11 +845,6 @@ class ValueSequence(JSONLike):
|
|
776
845
|
f")"
|
777
846
|
)
|
778
847
|
|
779
|
-
def __eq__(self, other: Any) -> bool:
|
780
|
-
if not isinstance(other, self.__class__):
|
781
|
-
return False
|
782
|
-
return self.to_dict() == other.to_dict()
|
783
|
-
|
784
848
|
def __deepcopy__(self, memo: dict[int, Any]):
|
785
849
|
kwargs = self.to_dict()
|
786
850
|
kwargs["values"] = kwargs.pop("_values")
|
@@ -804,31 +868,6 @@ class ValueSequence(JSONLike):
|
|
804
868
|
|
805
869
|
return obj
|
806
870
|
|
807
|
-
@classmethod
|
808
|
-
def from_json_like(cls, json_like, shared_data=None):
|
809
|
-
if "::" in json_like["path"]:
|
810
|
-
path, cls_method = json_like["path"].split("::")
|
811
|
-
json_like["path"] = path
|
812
|
-
json_like["value_class_method"] = cls_method
|
813
|
-
|
814
|
-
val_key = next((item for item in json_like if "values" in item), "")
|
815
|
-
if "::" in val_key:
|
816
|
-
# class method (e.g. `from_range`, `from_file` etc):
|
817
|
-
_, method = val_key.split("::")
|
818
|
-
_values_method_args = json_like.pop(val_key)
|
819
|
-
_values_method = f"_values_{method}"
|
820
|
-
_values_method_args = _process_demo_data_strings(
|
821
|
-
cls._app, _values_method_args
|
822
|
-
)
|
823
|
-
json_like["values"] = getattr(cls, _values_method)(**_values_method_args)
|
824
|
-
|
825
|
-
obj = super().from_json_like(json_like, shared_data)
|
826
|
-
if "::" in val_key:
|
827
|
-
obj._values_method = method
|
828
|
-
obj._values_method_args = _values_method_args
|
829
|
-
|
830
|
-
return obj
|
831
|
-
|
832
871
|
@property
|
833
872
|
def parameter(self) -> Parameter | None:
|
834
873
|
"""
|
@@ -839,7 +878,7 @@ class ValueSequence(JSONLike):
|
|
839
878
|
@property
|
840
879
|
def path_split(self) -> Sequence[str]:
|
841
880
|
"""
|
842
|
-
The components of
|
881
|
+
The components of this path.
|
843
882
|
"""
|
844
883
|
if self._path_split is None:
|
845
884
|
self._path_split = self.path.split(".")
|
@@ -1064,14 +1103,16 @@ class ValueSequence(JSONLike):
|
|
1064
1103
|
The workflow containing this sequence.
|
1065
1104
|
"""
|
1066
1105
|
if self._workflow:
|
1106
|
+
# (assigned in `make_persistent`)
|
1067
1107
|
return self._workflow
|
1068
1108
|
elif self._element_set:
|
1109
|
+
# (assigned by parent `ElementSet`)
|
1069
1110
|
if tmpl := self._element_set.task_template.workflow_template:
|
1070
1111
|
return tmpl.workflow
|
1071
1112
|
return None
|
1072
1113
|
|
1073
1114
|
@property
|
1074
|
-
def values(self) ->
|
1115
|
+
def values(self) -> Sequence[Any] | None:
|
1075
1116
|
"""
|
1076
1117
|
The values in this sequence.
|
1077
1118
|
"""
|
@@ -1355,6 +1396,253 @@ class ValueSequence(JSONLike):
|
|
1355
1396
|
return obj
|
1356
1397
|
|
1357
1398
|
|
1399
|
+
class MultiPathSequence(_BaseSequence):
|
1400
|
+
"""
|
1401
|
+
A sequence of values to be distributed across one or more paths.
|
1402
|
+
|
1403
|
+
Notes
|
1404
|
+
-----
|
1405
|
+
This is useful when we would like to generate values for multiple input paths that
|
1406
|
+
have some interdependency, or when they must be generate together in one go.
|
1407
|
+
|
1408
|
+
Parameters
|
1409
|
+
----------
|
1410
|
+
paths:
|
1411
|
+
The paths to this multi-path sequence.
|
1412
|
+
values:
|
1413
|
+
The values in this multi-path sequence.
|
1414
|
+
nesting_order: int
|
1415
|
+
A nesting order for this multi-path sequence. Can be used to compose sequences
|
1416
|
+
together.
|
1417
|
+
label: str
|
1418
|
+
A label for this multi-path sequence.
|
1419
|
+
value_class_method: str
|
1420
|
+
Name of a method used to generate multi-path sequence values. Not normally used
|
1421
|
+
directly.
|
1422
|
+
"""
|
1423
|
+
|
1424
|
+
# TODO: add a `path_axis` argument with doc string like:
|
1425
|
+
# path_axis:
|
1426
|
+
# The axis (as in a Numpy axis) along `values` to which the different paths
|
1427
|
+
# correspond.
|
1428
|
+
|
1429
|
+
def __init__(
|
1430
|
+
self,
|
1431
|
+
paths: Sequence[str],
|
1432
|
+
values: NDArray | Sequence[Sequence] | None,
|
1433
|
+
nesting_order: int | float | None = None,
|
1434
|
+
label: str | int | None = None,
|
1435
|
+
value_class_method: str | None = None,
|
1436
|
+
):
|
1437
|
+
self.paths = list(paths)
|
1438
|
+
self.nesting_order = nesting_order
|
1439
|
+
self.label = label
|
1440
|
+
self.value_class_method = value_class_method
|
1441
|
+
|
1442
|
+
self._sequences: list[ValueSequence] | None = None
|
1443
|
+
self._values: NDArray | Sequence[Sequence] | None = None
|
1444
|
+
|
1445
|
+
if values is not None:
|
1446
|
+
if (len_paths := len(paths)) != (len_vals := len(values)):
|
1447
|
+
raise ValueError(
|
1448
|
+
f"The number of values ({len_vals}) must be equal to the number of "
|
1449
|
+
f"paths provided ({len_paths})."
|
1450
|
+
)
|
1451
|
+
self._values = values
|
1452
|
+
self._sequences = [
|
1453
|
+
self._app.ValueSequence(
|
1454
|
+
path=path,
|
1455
|
+
values=values[idx],
|
1456
|
+
label=label,
|
1457
|
+
nesting_order=nesting_order,
|
1458
|
+
value_class_method=value_class_method,
|
1459
|
+
)
|
1460
|
+
for idx, path in enumerate(paths)
|
1461
|
+
]
|
1462
|
+
|
1463
|
+
# assigned by `_move_to_sequence_list` (invoked by first init of parent
|
1464
|
+
# `ElementSet`), corresponds to the sequence indices with the element set's
|
1465
|
+
# sequence list:
|
1466
|
+
self._sequence_indices: Sequence[int] | None = None
|
1467
|
+
|
1468
|
+
self._element_set: ElementSet | None = None # assigned by parent `ElementSet`
|
1469
|
+
|
1470
|
+
self._values_method: str | None = None
|
1471
|
+
self._values_method_args: dict | None = None
|
1472
|
+
|
1473
|
+
def __repr__(self):
|
1474
|
+
|
1475
|
+
label_str = f"label={self.label!r}, " if self.label else ""
|
1476
|
+
val_cls_str = (
|
1477
|
+
f"value_class_method={self.value_class_method!r}, "
|
1478
|
+
if self.value_class_method
|
1479
|
+
else ""
|
1480
|
+
)
|
1481
|
+
return (
|
1482
|
+
f"{self.__class__.__name__}("
|
1483
|
+
f"paths={self.paths!r}, "
|
1484
|
+
f"{label_str}"
|
1485
|
+
f"nesting_order={self.nesting_order}, "
|
1486
|
+
f"{val_cls_str}"
|
1487
|
+
f"values={self.values}"
|
1488
|
+
f")"
|
1489
|
+
)
|
1490
|
+
|
1491
|
+
def __deepcopy__(self, memo: dict[int, Any]):
|
1492
|
+
kwargs = self.to_dict()
|
1493
|
+
kwargs["values"] = kwargs.pop("_values")
|
1494
|
+
|
1495
|
+
_sequences = kwargs.pop("_sequences", None)
|
1496
|
+
_sequence_indices = kwargs.pop("_sequence_indices", None)
|
1497
|
+
_values_method = kwargs.pop("_values_method", None)
|
1498
|
+
_values_method_args = kwargs.pop("_values_method_args", None)
|
1499
|
+
|
1500
|
+
obj = self.__class__(**copy.deepcopy(kwargs, memo))
|
1501
|
+
|
1502
|
+
obj._sequences = _sequences
|
1503
|
+
obj._sequence_indices = _sequence_indices
|
1504
|
+
obj._values_method = _values_method
|
1505
|
+
obj._values_method_args = _values_method_args
|
1506
|
+
|
1507
|
+
obj._element_set = self._element_set
|
1508
|
+
|
1509
|
+
return obj
|
1510
|
+
|
1511
|
+
@override
|
1512
|
+
def _postprocess_to_dict(self, d: dict[str, Any]) -> dict[str, Any]:
|
1513
|
+
dct = super()._postprocess_to_dict(d)
|
1514
|
+
del dct["_sequences"]
|
1515
|
+
return dct
|
1516
|
+
|
1517
|
+
@classmethod
|
1518
|
+
def _json_like_constructor(cls, json_like):
|
1519
|
+
"""Invoked by `JSONLike.from_json_like` instead of `__init__`."""
|
1520
|
+
|
1521
|
+
# pop the keys we don't accept in `__init__`, and then assign after `__init__`:
|
1522
|
+
_sequence_indices = json_like.pop("_sequence_indices", None)
|
1523
|
+
|
1524
|
+
_values_method = json_like.pop("_values_method", None)
|
1525
|
+
_values_method_args = json_like.pop("_values_method_args", None)
|
1526
|
+
if "_values" in json_like:
|
1527
|
+
json_like["values"] = json_like.pop("_values")
|
1528
|
+
|
1529
|
+
obj = cls(**json_like)
|
1530
|
+
obj._sequence_indices = _sequence_indices
|
1531
|
+
obj._values_method = _values_method
|
1532
|
+
obj._values_method_args = _values_method_args
|
1533
|
+
return obj
|
1534
|
+
|
1535
|
+
@property
|
1536
|
+
def sequence_indices(self) -> Sequence[int] | None:
|
1537
|
+
"""
|
1538
|
+
The range indices (start and stop) to the parent element set's sequences list that
|
1539
|
+
correspond to the `ValueSequence`s generated by this multi-path sequence, if this
|
1540
|
+
object is bound to a parent element set.
|
1541
|
+
"""
|
1542
|
+
return self._sequence_indices
|
1543
|
+
|
1544
|
+
@property
|
1545
|
+
def sequences(self) -> Sequence[ValueSequence]:
|
1546
|
+
"""
|
1547
|
+
The child value sequences, one for each path.
|
1548
|
+
"""
|
1549
|
+
if self._sequence_indices:
|
1550
|
+
# they are stored in the parent `ElementSet`
|
1551
|
+
assert self._element_set
|
1552
|
+
return self._element_set.sequences[slice(*self._sequence_indices)]
|
1553
|
+
else:
|
1554
|
+
# not yet bound to a parent `ElementSet`
|
1555
|
+
assert self._sequences
|
1556
|
+
return self._sequences
|
1557
|
+
|
1558
|
+
@property
|
1559
|
+
def values(self) -> list[Sequence[Any]]:
|
1560
|
+
values = []
|
1561
|
+
for seq_i in self.sequences:
|
1562
|
+
assert seq_i.values
|
1563
|
+
values.append(seq_i.values)
|
1564
|
+
return values
|
1565
|
+
|
1566
|
+
def _move_to_sequence_list(self, sequences: list[ValueSequence]) -> None:
|
1567
|
+
"""
|
1568
|
+
Move the individual value sequences to an external list of value sequences (i.e.,
|
1569
|
+
the parent `ElementSet`'s), and update the `sequence_indices` attribute so we can
|
1570
|
+
retrieve the sequences from that list at will.
|
1571
|
+
"""
|
1572
|
+
len_ours = len(self.sequences)
|
1573
|
+
len_ext = len(sequences)
|
1574
|
+
sequences.extend(self.sequences)
|
1575
|
+
|
1576
|
+
# child sequences are now stored externally, and values retrieved via those:
|
1577
|
+
self._sequences = None
|
1578
|
+
self._values = None
|
1579
|
+
self._sequence_indices = [len_ext, len_ext + len_ours]
|
1580
|
+
|
1581
|
+
@classmethod
|
1582
|
+
def _values_from_latin_hypercube(
|
1583
|
+
cls,
|
1584
|
+
paths: Sequence[str],
|
1585
|
+
num_samples: int,
|
1586
|
+
*,
|
1587
|
+
scramble: bool = True,
|
1588
|
+
strength: int = 1,
|
1589
|
+
optimization: Literal["random-cd", "lloyd"] | None = None,
|
1590
|
+
rng=None,
|
1591
|
+
) -> NDArray:
|
1592
|
+
|
1593
|
+
num_paths = len(paths)
|
1594
|
+
kwargs = dict(
|
1595
|
+
d=num_paths,
|
1596
|
+
scramble=scramble,
|
1597
|
+
strength=strength,
|
1598
|
+
optimization=optimization,
|
1599
|
+
rng=rng,
|
1600
|
+
)
|
1601
|
+
try:
|
1602
|
+
sampler = LatinHypercube(**kwargs)
|
1603
|
+
except TypeError:
|
1604
|
+
# `rng` was previously (<1.15.0) `seed`:
|
1605
|
+
kwargs["seed"] = kwargs.pop("rng")
|
1606
|
+
sampler = LatinHypercube(**kwargs)
|
1607
|
+
return sampler.random(n=num_samples).T
|
1608
|
+
|
1609
|
+
@classmethod
|
1610
|
+
def from_latin_hypercube(
|
1611
|
+
cls,
|
1612
|
+
paths: Sequence[str],
|
1613
|
+
num_samples: int,
|
1614
|
+
*,
|
1615
|
+
scramble: bool = True,
|
1616
|
+
strength: int = 1,
|
1617
|
+
optimization: Literal["random-cd", "lloyd"] | None = None,
|
1618
|
+
rng=None,
|
1619
|
+
nesting_order: int | float | None = None,
|
1620
|
+
label: str | int | None = None,
|
1621
|
+
) -> Self:
|
1622
|
+
"""
|
1623
|
+
Generate values from SciPy's latin hypercube sampler: :class:`scipy.stats.qmc.LatinHypercube`.
|
1624
|
+
"""
|
1625
|
+
kwargs = {
|
1626
|
+
"paths": paths,
|
1627
|
+
"num_samples": num_samples,
|
1628
|
+
"scramble": scramble,
|
1629
|
+
"strength": strength,
|
1630
|
+
"optimization": optimization,
|
1631
|
+
"rng": rng,
|
1632
|
+
}
|
1633
|
+
values = cls._values_from_latin_hypercube(**kwargs)
|
1634
|
+
assert values is not None
|
1635
|
+
obj = cls(
|
1636
|
+
paths=paths,
|
1637
|
+
values=values,
|
1638
|
+
nesting_order=nesting_order,
|
1639
|
+
label=label,
|
1640
|
+
)
|
1641
|
+
obj._values_method = "from_latin_hypercube"
|
1642
|
+
obj._values_method_args = kwargs
|
1643
|
+
return obj
|
1644
|
+
|
1645
|
+
|
1358
1646
|
@dataclass
|
1359
1647
|
class AbstractInputValue(JSONLike):
|
1360
1648
|
"""Class to represent all sequence-able inputs to a task."""
|
@@ -1665,11 +1953,15 @@ class InputValue(AbstractInputValue):
|
|
1665
1953
|
json_like["label"] = label
|
1666
1954
|
|
1667
1955
|
if "::" in json_like["parameter"]:
|
1956
|
+
# double-colon syntax indicates a `ParameterValue`-subclass class method
|
1957
|
+
# of the specified name should be used to construct the values:
|
1668
1958
|
param, cls_method = json_like["parameter"].split("::")
|
1669
1959
|
json_like["parameter"] = param
|
1670
1960
|
json_like["value_class_method"] = cls_method
|
1671
1961
|
|
1672
1962
|
if "path" not in json_like:
|
1963
|
+
# in the case this value corresponds to some sub-part of the parameter's
|
1964
|
+
# nested data structure:
|
1673
1965
|
param, *path = json_like["parameter"].split(".")
|
1674
1966
|
json_like["parameter"] = param
|
1675
1967
|
json_like["path"] = ".".join(path)
|
@@ -1726,6 +2018,12 @@ class ResourceSpec(JSONLike):
|
|
1726
2018
|
Whether to use array jobs.
|
1727
2019
|
max_array_items: int
|
1728
2020
|
If using array jobs, up to how many items should be in the job array.
|
2021
|
+
write_app_logs: bool
|
2022
|
+
Whether an app log file should be written.
|
2023
|
+
combine_jobscript_std: bool
|
2024
|
+
Whether jobscript standard output and error streams should be combined.
|
2025
|
+
combine_scripts: bool
|
2026
|
+
Whether Python scripts should be combined.
|
1729
2027
|
time_limit: str
|
1730
2028
|
How long to run for.
|
1731
2029
|
scheduler_args: dict[str, Any]
|
@@ -1736,6 +2034,13 @@ class ResourceSpec(JSONLike):
|
|
1736
2034
|
Which OS to use.
|
1737
2035
|
environments: dict
|
1738
2036
|
Which execution environments to use.
|
2037
|
+
resources_id: int
|
2038
|
+
An arbitrary integer that can be used to force multiple jobscripts.
|
2039
|
+
skip_downstream_on_failure: bool
|
2040
|
+
Whether to skip downstream dependents on failure.
|
2041
|
+
allow_failed_dependencies: int | float | bool | None
|
2042
|
+
The failure tolerance with respect to dependencies, specified as a number or
|
2043
|
+
proportion.
|
1739
2044
|
SGE_parallel_env: str
|
1740
2045
|
Which SGE parallel environment to request.
|
1741
2046
|
SLURM_partition: str
|
@@ -1762,11 +2067,16 @@ class ResourceSpec(JSONLike):
|
|
1762
2067
|
"shell",
|
1763
2068
|
"use_job_array",
|
1764
2069
|
"max_array_items",
|
2070
|
+
"write_app_logs",
|
2071
|
+
"combine_jobscript_std",
|
2072
|
+
"combine_scripts",
|
1765
2073
|
"time_limit",
|
1766
2074
|
"scheduler_args",
|
1767
2075
|
"shell_args",
|
1768
2076
|
"os_name",
|
1769
2077
|
"environments",
|
2078
|
+
"resources_id",
|
2079
|
+
"skip_downstream_on_failure",
|
1770
2080
|
"SGE_parallel_env",
|
1771
2081
|
"SLURM_partition",
|
1772
2082
|
"SLURM_num_tasks",
|
@@ -1819,11 +2129,16 @@ class ResourceSpec(JSONLike):
|
|
1819
2129
|
shell: str | None = None,
|
1820
2130
|
use_job_array: bool | None = None,
|
1821
2131
|
max_array_items: int | None = None,
|
2132
|
+
write_app_logs: bool | None = None,
|
2133
|
+
combine_jobscript_std: bool | None = None,
|
2134
|
+
combine_scripts: bool | None = None,
|
1822
2135
|
time_limit: str | timedelta | None = None,
|
1823
2136
|
scheduler_args: dict[str, Any] | None = None,
|
1824
2137
|
shell_args: dict[str, Any] | None = None,
|
1825
2138
|
os_name: str | None = None,
|
1826
2139
|
environments: Mapping[str, Mapping[str, Any]] | None = None,
|
2140
|
+
resources_id: int | None = None,
|
2141
|
+
skip_downstream_on_failure: bool | None = None,
|
1827
2142
|
SGE_parallel_env: str | None = None,
|
1828
2143
|
SLURM_partition: str | None = None,
|
1829
2144
|
SLURM_num_tasks: str | None = None,
|
@@ -1852,8 +2167,13 @@ class ResourceSpec(JSONLike):
|
|
1852
2167
|
self._shell = self._process_string(shell)
|
1853
2168
|
self._os_name = self._process_string(os_name)
|
1854
2169
|
self._environments = environments
|
2170
|
+
self._resources_id = resources_id
|
2171
|
+
self._skip_downstream_on_failure = skip_downstream_on_failure
|
1855
2172
|
self._use_job_array = use_job_array
|
1856
2173
|
self._max_array_items = max_array_items
|
2174
|
+
self._write_app_logs = write_app_logs
|
2175
|
+
self._combine_jobscript_std = combine_jobscript_std
|
2176
|
+
self._combine_scripts = combine_scripts
|
1857
2177
|
self._time_limit = time_limit
|
1858
2178
|
self._scheduler_args = scheduler_args
|
1859
2179
|
self._shell_args = shell_args
|
@@ -1991,11 +2311,16 @@ class ResourceSpec(JSONLike):
|
|
1991
2311
|
self._shell = None
|
1992
2312
|
self._use_job_array = None
|
1993
2313
|
self._max_array_items = None
|
2314
|
+
self._write_app_logs = None
|
2315
|
+
self._combine_jobscript_std = None
|
2316
|
+
self._combine_scripts = None
|
1994
2317
|
self._time_limit = None
|
1995
2318
|
self._scheduler_args = None
|
1996
2319
|
self._shell_args = None
|
1997
2320
|
self._os_name = None
|
1998
2321
|
self._environments = None
|
2322
|
+
self._resources_id = None
|
2323
|
+
self._skip_downstream_on_failure = None
|
1999
2324
|
|
2000
2325
|
return (self.normalised_path, [data_ref], is_new)
|
2001
2326
|
|
@@ -2110,6 +2435,18 @@ class ResourceSpec(JSONLike):
|
|
2110
2435
|
"""
|
2111
2436
|
return self._get_value("max_array_items")
|
2112
2437
|
|
2438
|
+
@property
|
2439
|
+
def write_app_logs(self) -> bool:
|
2440
|
+
return self._get_value("write_app_logs")
|
2441
|
+
|
2442
|
+
@property
|
2443
|
+
def combine_jobscript_std(self) -> bool:
|
2444
|
+
return self._get_value("combine_jobscript_std")
|
2445
|
+
|
2446
|
+
@property
|
2447
|
+
def combine_scripts(self) -> bool:
|
2448
|
+
return self._get_value("combine_scripts")
|
2449
|
+
|
2113
2450
|
@property
|
2114
2451
|
def time_limit(self) -> str | None:
|
2115
2452
|
"""
|
@@ -2150,6 +2487,14 @@ class ResourceSpec(JSONLike):
|
|
2150
2487
|
"""
|
2151
2488
|
return self._get_value("environments")
|
2152
2489
|
|
2490
|
+
@property
|
2491
|
+
def resources_id(self) -> int:
|
2492
|
+
return self._get_value("resources_id")
|
2493
|
+
|
2494
|
+
@property
|
2495
|
+
def skip_downstream_on_failure(self) -> bool:
|
2496
|
+
return self._get_value("skip_downstream_on_failure")
|
2497
|
+
|
2153
2498
|
@property
|
2154
2499
|
def SGE_parallel_env(self) -> str | None:
|
2155
2500
|
"""
|