hpcflow-new2 0.2.0a190__py3-none-any.whl → 0.2.0a200__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +1 -0
  2. hpcflow/_version.py +1 -1
  3. hpcflow/data/scripts/bad_script.py +2 -0
  4. hpcflow/data/scripts/do_nothing.py +2 -0
  5. hpcflow/data/scripts/env_specifier_test/input_file_generator_pass_env_spec.py +4 -0
  6. hpcflow/data/scripts/env_specifier_test/main_script_test_pass_env_spec.py +8 -0
  7. hpcflow/data/scripts/env_specifier_test/output_file_parser_pass_env_spec.py +4 -0
  8. hpcflow/data/scripts/env_specifier_test/v1/input_file_generator_basic.py +4 -0
  9. hpcflow/data/scripts/env_specifier_test/v1/main_script_test_direct_in_direct_out.py +7 -0
  10. hpcflow/data/scripts/env_specifier_test/v1/output_file_parser_basic.py +4 -0
  11. hpcflow/data/scripts/env_specifier_test/v2/main_script_test_direct_in_direct_out.py +7 -0
  12. hpcflow/data/scripts/input_file_generator_basic.py +3 -0
  13. hpcflow/data/scripts/input_file_generator_basic_FAIL.py +3 -0
  14. hpcflow/data/scripts/input_file_generator_test_stdout_stderr.py +8 -0
  15. hpcflow/data/scripts/main_script_test_direct_in.py +3 -0
  16. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2.py +6 -0
  17. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed.py +6 -0
  18. hpcflow/data/scripts/main_script_test_direct_in_direct_out_2_fail_allowed_group.py +7 -0
  19. hpcflow/data/scripts/main_script_test_direct_in_direct_out_3.py +6 -0
  20. hpcflow/data/scripts/main_script_test_direct_in_group_direct_out_3.py +6 -0
  21. hpcflow/data/scripts/main_script_test_direct_in_group_one_fail_direct_out_3.py +6 -0
  22. hpcflow/data/scripts/main_script_test_hdf5_in_obj_2.py +12 -0
  23. hpcflow/data/scripts/main_script_test_json_out_FAIL.py +3 -0
  24. hpcflow/data/scripts/main_script_test_shell_env_vars.py +12 -0
  25. hpcflow/data/scripts/main_script_test_std_out_std_err.py +6 -0
  26. hpcflow/data/scripts/output_file_parser_basic.py +3 -0
  27. hpcflow/data/scripts/output_file_parser_basic_FAIL.py +7 -0
  28. hpcflow/data/scripts/output_file_parser_test_stdout_stderr.py +8 -0
  29. hpcflow/data/scripts/script_exit_test.py +5 -0
  30. hpcflow/data/template_components/environments.yaml +1 -1
  31. hpcflow/sdk/__init__.py +5 -0
  32. hpcflow/sdk/app.py +166 -92
  33. hpcflow/sdk/cli.py +263 -84
  34. hpcflow/sdk/cli_common.py +99 -5
  35. hpcflow/sdk/config/callbacks.py +38 -1
  36. hpcflow/sdk/config/config.py +102 -13
  37. hpcflow/sdk/config/errors.py +19 -5
  38. hpcflow/sdk/config/types.py +3 -0
  39. hpcflow/sdk/core/__init__.py +25 -1
  40. hpcflow/sdk/core/actions.py +914 -262
  41. hpcflow/sdk/core/cache.py +76 -34
  42. hpcflow/sdk/core/command_files.py +14 -128
  43. hpcflow/sdk/core/commands.py +35 -6
  44. hpcflow/sdk/core/element.py +122 -50
  45. hpcflow/sdk/core/errors.py +58 -2
  46. hpcflow/sdk/core/execute.py +207 -0
  47. hpcflow/sdk/core/loop.py +408 -50
  48. hpcflow/sdk/core/loop_cache.py +4 -4
  49. hpcflow/sdk/core/parameters.py +382 -37
  50. hpcflow/sdk/core/run_dir_files.py +13 -40
  51. hpcflow/sdk/core/skip_reason.py +7 -0
  52. hpcflow/sdk/core/task.py +119 -30
  53. hpcflow/sdk/core/task_schema.py +68 -0
  54. hpcflow/sdk/core/test_utils.py +66 -27
  55. hpcflow/sdk/core/types.py +54 -1
  56. hpcflow/sdk/core/utils.py +136 -19
  57. hpcflow/sdk/core/workflow.py +1587 -356
  58. hpcflow/sdk/data/workflow_spec_schema.yaml +2 -0
  59. hpcflow/sdk/demo/cli.py +7 -0
  60. hpcflow/sdk/helper/cli.py +1 -0
  61. hpcflow/sdk/log.py +42 -15
  62. hpcflow/sdk/persistence/base.py +405 -53
  63. hpcflow/sdk/persistence/json.py +177 -52
  64. hpcflow/sdk/persistence/pending.py +237 -69
  65. hpcflow/sdk/persistence/store_resource.py +3 -2
  66. hpcflow/sdk/persistence/types.py +15 -4
  67. hpcflow/sdk/persistence/zarr.py +928 -81
  68. hpcflow/sdk/submission/jobscript.py +1408 -489
  69. hpcflow/sdk/submission/schedulers/__init__.py +40 -5
  70. hpcflow/sdk/submission/schedulers/direct.py +33 -19
  71. hpcflow/sdk/submission/schedulers/sge.py +51 -16
  72. hpcflow/sdk/submission/schedulers/slurm.py +44 -16
  73. hpcflow/sdk/submission/schedulers/utils.py +7 -2
  74. hpcflow/sdk/submission/shells/base.py +68 -20
  75. hpcflow/sdk/submission/shells/bash.py +222 -129
  76. hpcflow/sdk/submission/shells/powershell.py +200 -150
  77. hpcflow/sdk/submission/submission.py +852 -119
  78. hpcflow/sdk/submission/types.py +18 -21
  79. hpcflow/sdk/typing.py +24 -5
  80. hpcflow/sdk/utils/arrays.py +71 -0
  81. hpcflow/sdk/utils/deferred_file.py +55 -0
  82. hpcflow/sdk/utils/hashing.py +16 -0
  83. hpcflow/sdk/utils/patches.py +12 -0
  84. hpcflow/sdk/utils/strings.py +33 -0
  85. hpcflow/tests/api/test_api.py +32 -0
  86. hpcflow/tests/conftest.py +19 -0
  87. hpcflow/tests/data/benchmark_script_runner.yaml +26 -0
  88. hpcflow/tests/data/multi_path_sequences.yaml +29 -0
  89. hpcflow/tests/data/workflow_test_run_abort.yaml +34 -35
  90. hpcflow/tests/schedulers/sge/test_sge_submission.py +36 -0
  91. hpcflow/tests/scripts/test_input_file_generators.py +282 -0
  92. hpcflow/tests/scripts/test_main_scripts.py +821 -70
  93. hpcflow/tests/scripts/test_non_snippet_script.py +46 -0
  94. hpcflow/tests/scripts/test_ouput_file_parsers.py +353 -0
  95. hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -0
  96. hpcflow/tests/unit/test_action.py +176 -0
  97. hpcflow/tests/unit/test_app.py +20 -0
  98. hpcflow/tests/unit/test_cache.py +46 -0
  99. hpcflow/tests/unit/test_cli.py +133 -0
  100. hpcflow/tests/unit/test_config.py +122 -1
  101. hpcflow/tests/unit/test_element_iteration.py +47 -0
  102. hpcflow/tests/unit/test_jobscript_unit.py +757 -0
  103. hpcflow/tests/unit/test_loop.py +1332 -27
  104. hpcflow/tests/unit/test_meta_task.py +325 -0
  105. hpcflow/tests/unit/test_multi_path_sequences.py +229 -0
  106. hpcflow/tests/unit/test_parameter.py +13 -0
  107. hpcflow/tests/unit/test_persistence.py +190 -8
  108. hpcflow/tests/unit/test_run.py +109 -3
  109. hpcflow/tests/unit/test_run_directories.py +29 -0
  110. hpcflow/tests/unit/test_shell.py +20 -0
  111. hpcflow/tests/unit/test_submission.py +5 -76
  112. hpcflow/tests/unit/test_workflow_template.py +31 -0
  113. hpcflow/tests/unit/utils/test_arrays.py +40 -0
  114. hpcflow/tests/unit/utils/test_deferred_file_writer.py +34 -0
  115. hpcflow/tests/unit/utils/test_hashing.py +65 -0
  116. hpcflow/tests/unit/utils/test_patches.py +5 -0
  117. hpcflow/tests/unit/utils/test_redirect_std.py +50 -0
  118. hpcflow/tests/workflows/__init__.py +0 -0
  119. hpcflow/tests/workflows/test_directory_structure.py +31 -0
  120. hpcflow/tests/workflows/test_jobscript.py +332 -0
  121. hpcflow/tests/workflows/test_run_status.py +198 -0
  122. hpcflow/tests/workflows/test_skip_downstream.py +696 -0
  123. hpcflow/tests/workflows/test_submission.py +140 -0
  124. hpcflow/tests/workflows/test_workflows.py +142 -2
  125. hpcflow/tests/workflows/test_zip.py +18 -0
  126. hpcflow/viz_demo.ipynb +6587 -3
  127. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/METADATA +7 -4
  128. hpcflow_new2-0.2.0a200.dist-info/RECORD +222 -0
  129. hpcflow_new2-0.2.0a190.dist-info/RECORD +0 -165
  130. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/LICENSE +0 -0
  131. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/WHEEL +0 -0
  132. {hpcflow_new2-0.2.0a190.dist-info → hpcflow_new2-0.2.0a200.dist-info}/entry_points.txt +0 -0
@@ -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 DependencyCache
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 = DependencyCache.build(workflow)
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 elem_idx in task.element_IDs:
249
- element = deps_cache.elements[elem_idx]
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,
@@ -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
- #: Whether to expect more than of these parameters defined in the workflow.
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 ValueSequence(JSONLike):
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: list[Any] | None,
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 ths path.
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) -> list[Any] | None:
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
  """