Skip to content

Commit 4929558

Browse files
committed
ArraySubmitterHelper - broken
1 parent 6a6c42a commit 4929558

7 files changed

Lines changed: 548 additions & 47 deletions

File tree

src/qq_lib/core/array_spec.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Released under MIT License.
22
# Copyright (c) 2025-2026 Ladislav Bartos and Robert Vacha Lab
33

4+
from collections.abc import Iterator, Sequence
5+
46
from qq_lib.core.error import QQError
57

68
type ArrayElement = int | tuple[int, int] | tuple[int, int, int]
@@ -11,23 +13,36 @@ class ArraySpec:
1113
Specification for job-array task indices.
1214
1315
Args:
14-
elements (list[ArrayElement]): Non-empty list of indices and ranges.
16+
elements (Sequence[ArrayElement]): Non-empty list of indices and ranges.
1517
1618
Raises:
1719
QQError: If the list is empty or any element violates constraints.
1820
"""
1921

20-
def __init__(self, elements: list[ArrayElement]):
22+
def __init__(self, elements: Sequence[ArrayElement]):
2123
_validate_elements(elements)
2224
self.elements = _merge_elements(elements)
2325

24-
25-
def _validate_elements(elements: list[ArrayElement]) -> None:
26+
def __iter__(self) -> Iterator[int]:
27+
"""
28+
Yields individual task indices from the array specification.
29+
"""
30+
for element in self.elements:
31+
match element:
32+
case int(value):
33+
yield value
34+
case (int(start), int(stop)):
35+
yield from range(start, stop + 1)
36+
case (int(start), int(stop), int(step)):
37+
yield from range(start, stop + 1, step)
38+
39+
40+
def _validate_elements(elements: Sequence[ArrayElement]) -> None:
2641
"""
2742
Validate a list of array elements.
2843
2944
Args:
30-
elements (list[ArrayElement]): Non-empty list of indices and ranges.
45+
elements (Sequence[ArrayElement]): Non-empty sequence of indices and ranges.
3146
3247
Raises:
3348
QQError: If an element has an unsupported type.
@@ -72,7 +87,7 @@ def _validate_elements(elements: list[ArrayElement]) -> None:
7287
)
7388

7489

75-
def _merge_elements(elements: list[ArrayElement]) -> list[ArrayElement]:
90+
def _merge_elements(elements: Sequence[ArrayElement]) -> list[ArrayElement]:
7691
"""
7792
Merge array elements into a shorter equivalent list.
7893
@@ -82,7 +97,7 @@ def _merge_elements(elements: list[ArrayElement]) -> list[ArrayElement]:
8297
will not be collapsed into `(1, 5, 2)`.
8398
8499
Args:
85-
elements (list[ArrayElement]): Non-empty list of indices and ranges.
100+
elements (Sequence[ArrayElement]): Non-empty sequence of indices and ranges.
86101
87102
Returns:
88103
list[ArrayElement]: Equivalent list with overlapping and adjacent

src/qq_lib/core/common.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"""
1010

1111
import re
12+
from collections.abc import Sequence
1213
from datetime import timedelta
1314
from functools import lru_cache
1415
from pathlib import Path
@@ -99,7 +100,7 @@ def get_info_file(directory: Path) -> Path:
99100
"""
100101
Locate the qq job info file in a directory.
101102
102-
This function searches for files matching the `QQ_INFO_SUFFIX` in the
103+
This function searches for files with suffix `CFG.suffixes.qq_info` in the
103104
provided directory. It raises an error if none or multiple info files are found.
104105
105106
Args:
@@ -124,7 +125,7 @@ def get_info_files(directory: Path) -> list[Path]:
124125
"""
125126
Retrieve all qq job info files in a directory.
126127
127-
This function searches for files matching the `QQ_INFO_SUFFIX` in the
128+
This function searches for files with suffix `CFG.suffixes.qq_info` in the
128129
provided directory. The files are sorted by their last modification time
129130
(with the newest modified file being last in the list).
130131
@@ -140,6 +141,51 @@ def get_info_files(directory: Path) -> list[Path]:
140141
return sorted(info_files, key=lambda f: f.stat().st_mtime)
141142

142143

144+
def get_array_file(directory: Path) -> Path:
145+
"""
146+
Located the qq array file in a directory.
147+
148+
This function searches for files with suffix `CFG.suffixes.qq_array` in the
149+
provided directory. It raises an error if none or multiple array files are found.
150+
151+
Args:
152+
directory (Path): The directory to search in.
153+
154+
Returns:
155+
Path: The Path object of the detected qq array file.
156+
157+
Raises:
158+
QQError: If no array file is found or multiple array files are detected.
159+
"""
160+
array_files = get_array_files(directory)
161+
if len(array_files) == 0:
162+
raise QQError("No qq array file found.")
163+
if len(array_files) > 1:
164+
raise QQError("Multiple qq array files found.")
165+
166+
return array_files[0]
167+
168+
169+
def get_array_files(directory: Path) -> list[Path]:
170+
"""
171+
Retrieve all qq array files in a directory.
172+
173+
This function searches for files with suffix `CFG.suffixes.qq_array` in the
174+
provided directory. The files are sorted by their last modification time
175+
(with the newest modified file being last in the list).
176+
177+
Args:
178+
directory (Path): The directory to search in.
179+
180+
Returns:
181+
list[Path]: A list of Path objects representing the detected qq array files.
182+
"""
183+
array_files = get_files_with_suffix(directory, CFG.suffixes.qq_array)
184+
logger.debug(f"Detected the following qq array files: {array_files}.")
185+
186+
return sorted(array_files, key=lambda f: f.stat().st_mtime)
187+
188+
143189
def get_info_file_from_job_id(job_id: str) -> Path:
144190
"""
145191
Get path to the qq info file corresponding to a job with the given ID.
@@ -766,3 +812,30 @@ def default_resubmit_from_hosts() -> str:
766812
# if no batch system is available
767813
except QQError:
768814
return "??? (no batch system detected)"
815+
816+
817+
def subset_indices(a: Sequence[str | Path], b: Sequence[str | Path]) -> list[int]:
818+
"""
819+
Return indices in *a* corresponding to each element of *b*.
820+
821+
*b* must be a subset of *a*; both lists are assumed to contain no
822+
duplicates.
823+
824+
Args:
825+
a (Sequence[str | Path]): The reference sequence of strings or Paths.
826+
b (Sequence[str | Path]): The query list whose elements must all appear in *a*.
827+
828+
Returns:
829+
list[int]: A list of the same length as *b* where the i-th entry is
830+
the index of `b[i]` in *a*.
831+
832+
Raises:
833+
QQError: If *b* contains elements not present in *a*.
834+
"""
835+
index_of: dict[str | Path, int] = {value: idx for idx, value in enumerate(a)}
836+
837+
missing: list[str | Path] = [item for item in b if item not in index_of]
838+
if missing:
839+
raise QQError(f"b is not a subset of a. Missing elements: {missing}.")
840+
841+
return [index_of[item] for item in b]

src/qq_lib/info/array_informer.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ def batch_system(self) -> type[BatchInterface]:
4040
"""
4141
return self.array_info.batch_system
4242

43+
@property
44+
def tasks(self) -> list[Informer | None]:
45+
"""
46+
Return the list of Informers for the tasks in this array.
47+
48+
Returns:
49+
list[Informer | None]: The list of Informers for the tasks in this array.
50+
"""
51+
return self._tasks
52+
4353
@classmethod
4454
def from_file(cls, file: Path, host: str | None = None) -> Self:
4555
"""
@@ -179,13 +189,18 @@ def all_tasks_in_state(self, states: list[RealState]) -> bool:
179189
if not self._batch_info_loaded:
180190
self.load_batch_info()
181191

182-
return all(informer.get_real_state() in states for informer in self._tasks)
192+
return all(
193+
informer is not None and informer.get_real_state() in states
194+
for informer in self._tasks
195+
)
183196

184197
@staticmethod
185-
def _get_informers_for_tasks(array_info: ArrayInfo) -> list[Informer]:
198+
def _get_informers_for_tasks(array_info: ArrayInfo) -> list[Informer | None]:
186199
"""
187200
Create informers for each task in the array job.
201+
188202
The informers are returned in the order corresponding to the directory order.
203+
If an informer cannot be created for a task, it is replaced with `None`,
189204
190205
Batch job info is NOT automatically loaded for the informers.
191206
@@ -199,7 +214,10 @@ def _get_informers_for_tasks(array_info: ArrayInfo) -> list[Informer]:
199214
informers = []
200215
for dir in array_info.task_dirs:
201216
info_path = construct_info_file_path(dir, array_info.job_name)
202-
informers.append(Informer.from_file(info_path))
217+
try:
218+
informers.append(Informer.from_file(info_path))
219+
except QQError:
220+
informers.append(None)
203221
return informers
204222

205223
def _get_batch_tasks(self) -> list[BatchJobInterface]:
@@ -272,6 +290,10 @@ def _match_batch_tasks_to_informers(
272290
Match batch tasks to informers based on task numbers.
273291
"""
274292
for informer in self._tasks:
293+
# skip directories for which informers are missing
294+
if informer is None:
295+
continue
296+
275297
# get task number from the informer to match it with the batch system info
276298
if (task_info := informer.info.task_info) is None:
277299
raise QQError(

0 commit comments

Comments
 (0)