Skip to content

Commit 48b7bd2

Browse files
authored
Merge pull request #7 from VachaLab/v0.9
v0.9
2 parents b8181a8 + 5fe2ee8 commit 48b7bd2

14 files changed

Lines changed: 293 additions & 93 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## Version 0.9.0
2+
- Added `continuous` jobs: a light-weight alternative to loop jobs. Continuous jobs automatically submit their continuation but do not track their cycle nor do they perform archival operations. See [the manual](https://vachalab.github.io/qq-manual/continuous_job.html) for more information.
3+
4+
***
5+
16
## Version 0.8.0
27
- Added the `--transfer-mode` and `--archive-mode` options, which allow automatically transferring (and archiving, respectively) files from the working directory for other jobs than those successfully finished. See [the manual](https://vachalab.github.io/qq-manual/transfer_modes.html) for more information.
38
- As a consequence of the above change, the behavior of `qq go`, `qq sync`, and `qq wipe` has been slightly adjusted.

scripts/qq_scripts/gmx-eta

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Requires `uv`: https://docs.astral.sh/uv
1616
# ]
1717
#
1818
# [tool.uv.sources]
19-
# qq = { git = "https://github.com/Ladme/qq.git", tag = "v0.8.0" }
19+
# qq = { git = "https://github.com/Ladme/qq.git", tag = "v0.9.0" }
2020
# ///
2121

2222
import argparse

scripts/qq_scripts/multi-check

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Requires `uv`: https://docs.astral.sh/uv
1616
# ]
1717
#
1818
# [tool.uv.sources]
19-
# qq = { git = "https://github.com/Ladme/qq.git", tag = "v0.8.0" }
19+
# qq = { git = "https://github.com/Ladme/qq.git", tag = "v0.9.0" }
2020
# ///
2121

2222
import argparse

scripts/qq_scripts/multi-kill

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Requires `uv`: https://docs.astral.sh/uv
1616
# ]
1717
#
1818
# [tool.uv.sources]
19-
# qq = { git = "https://github.com/Ladme/qq.git", tag = "v0.8.0" }
19+
# qq = { git = "https://github.com/Ladme/qq.git", tag = "v0.9.0" }
2020
# ///
2121

2222
import argparse

scripts/qq_scripts/multi-submit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Requires `uv`: https://docs.astral.sh/uv
1616
# ]
1717
#
1818
# [tool.uv.sources]
19-
# qq = { git = "https://github.com/Ladme/qq.git", tag = "v0.8.0" }
19+
# qq = { git = "https://github.com/Ladme/qq.git", tag = "v0.9.0" }
2020
# ///
2121

2222
import argparse

src/qq_lib/core/common.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,3 +725,15 @@ def available_work_dirs() -> str:
725725
return ", ".join([f"'{work_dir_type}'" for work_dir_type in work_dirs])
726726
except QQError:
727727
return "??? (no batch system detected)"
728+
729+
730+
def available_job_types() -> str:
731+
"""
732+
Return the supported job types.
733+
734+
Returns:
735+
str: A comma-separated list of supported job types, each wrapped in quotes.
736+
"""
737+
from qq_lib.properties.job_type import JobType
738+
739+
return ", ".join([f"'{str(job_type)}'" for job_type in JobType])

src/qq_lib/properties/job_type.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class JobType(Enum):
2121

2222
STANDARD = 1
2323
LOOP = 2
24+
CONTINUOUS = 3
2425

2526
def __str__(self):
2627
return self.name.lower()

src/qq_lib/run/runner.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ def __init__(self, info_file: Path, host: str):
129129
else:
130130
self._archiver = None
131131

132+
if self._informer.info.job_type == JobType.CONTINUOUS:
133+
self._should_resubmit = True
134+
132135
def prepare(self) -> None:
133136
"""
134137
Prepare the script for execution, setting up the archive
@@ -211,7 +214,7 @@ def execute(self) -> int:
211214
# if the script returns an exit code corresponding to CFG.exit_codes.qq_run_no_resubmit,
212215
# do not submit the next cycle of the job but return 0
213216
if (
214-
self._informer.info.loop_info is not None
217+
self._informer.info.job_type in [JobType.LOOP, JobType.CONTINUOUS]
215218
and self._process.returncode == CFG.exit_codes.qq_run_no_resubmit
216219
):
217220
logger.debug(
@@ -243,7 +246,7 @@ def finalize(self) -> None:
243246
- If not using scratch: No file operations are performed.
244247
3. Updates the qq info file to "finished" (exit code 0) or "failed" (non-zero
245248
exit code).
246-
4. Resubmits the job if it is a loop job and completed successfully.
249+
4. Resubmits the job if it is a loop or continuous job and was completed successfully.
247250
248251
Raises:
249252
QQError: If copying, deletion, or archiving of files fails or if the resubmission fails.
@@ -291,8 +294,8 @@ def finalize(self) -> None:
291294
# update the qqinfo file
292295
self._updateInfoFinished()
293296

294-
# if this is a loop job
295-
if self._informer.info.job_type == JobType.LOOP:
297+
# if this is a loop/continuous job
298+
if self._informer.info.job_type in [JobType.LOOP, JobType.CONTINUOUS]:
296299
self._resubmit()
297300
else:
298301
# update the qqinfo file
@@ -646,25 +649,32 @@ def _reloadInfoAndEnsureValid(self, retry: bool = False) -> None:
646649

647650
def _resubmit(self) -> None:
648651
"""
649-
Resubmit the current loop job to the batch system if additional cycles remain.
652+
Resubmit the current job if either of the following is true:
653+
a) it is a loop job and additional cycles remain,
654+
b) it is a continuous job that should be resubmitted.
650655
651656
Raises:
652657
QQError: If the job cannot be resubmitted.
653658
"""
654-
if not (loop_info := self._informer.info.loop_info):
655-
logger.debug("Loop info is undefined while resubmiting. This is a bug!")
656-
return
657-
658-
if loop_info.current >= loop_info.end:
659-
logger.info("This was the final cycle of the loop job. Not resubmitting.")
660-
return
661-
662659
if not self._should_resubmit:
663660
logger.info(
664661
f"The script finished with an exit code of '{CFG.exit_codes.qq_run_no_resubmit}' indicating that the next cycle of the job should not be submitted. Not resubmitting."
665662
)
666663
return
667664

665+
if self._informer.info.job_type == JobType.LOOP:
666+
if not (loop_info := self._informer.info.loop_info):
667+
logger.warning(
668+
"Loop info is undefined while resubmiting a loop job. This is a bug!"
669+
)
670+
return
671+
672+
if loop_info.current >= loop_info.end:
673+
logger.info(
674+
"This was the final cycle of the loop job. Not resubmitting."
675+
)
676+
return
677+
668678
logger.info("Resubmitting the job.")
669679
logger.debug(
670680
f"Resubmitting using the batch system '{str(self._batch_system)}'."

src/qq_lib/submit/cli.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
from click_option_group import optgroup
1212

1313
from qq_lib.core.click_format import GNUHelpColorsCommand
14-
from qq_lib.core.common import available_work_dirs, get_runtime_files
14+
from qq_lib.core.common import (
15+
available_job_types,
16+
available_work_dirs,
17+
get_runtime_files,
18+
)
1519
from qq_lib.core.config import CFG
1620
from qq_lib.core.error import QQError
1721
from qq_lib.core.logger import get_logger
@@ -86,7 +90,7 @@ def complete_script(
8690
"--job-type",
8791
type=str,
8892
default=None,
89-
help="Type of the qq job. Defaults to 'standard'.",
93+
help=f"Type of the qq job. Defaults to 'standard'. Available types: {available_job_types()}.",
9094
)
9195
@optgroup.option(
9296
"--exclude",

src/qq_lib/submit/submitter.py

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -183,41 +183,69 @@ def submit(self) -> str:
183183

184184
def continuesLoop(self) -> bool:
185185
"""
186-
Determine whether the submitted job is a continuation of a loop job.
187-
188-
Checks if an info file exists in the input directory that corresponds
189-
to the previous cycle of the same loop job. A job is considered a valid
190-
continuation if:
191-
- An info file is found.
192-
- Both the info file and the current job are loop jobs.
193-
- The previous job finished successfully.
194-
- The previous loop cycle number is exactly one less than the current one.
186+
Determine whether the submitted job is a continuation of a loop/continuous job.
195187
196188
Returns:
197-
bool: True if the job is a valid continuation of a previous loop job,
189+
bool: True if the job is a valid continuation of a previous loop/continuous job,
198190
False otherwise.
199191
"""
200192
try:
201-
# only one qq info file can be present
193+
# there should only be one info file for both loop jobs (runtime files are archived)
194+
# and continuous jobs (runtime files overwrite each other)
202195
info_file = get_info_file(self._input_dir)
203196
informer = Informer.fromFile(info_file)
204197

205-
if (
206-
informer.info.loop_info
207-
and self._loop_info
208-
and informer.info.job_state == NaiveState.FINISHED
209-
and informer.info.loop_info.current == self._loop_info.current - 1
198+
if self._loopJobContinuesLoop(informer) or self._continuousJobContinuesLoop(
199+
informer
210200
):
211-
logger.debug("Valid loop job with a correct cycle.")
201+
logger.debug("Valid loop job with a correct cycle or a continuous job.")
212202
return True
213203
logger.debug(
214-
"Detected info file is either not a loop job or does not correspond to the previous cycle."
204+
"Detected info file does not correspond to a resubmittable job."
215205
)
216206
return False
217207
except QQError as e:
218208
logger.debug(f"Could not read an info file: {e}.")
219209
return False
220210

211+
def _loopJobContinuesLoop(self, previous: Informer) -> bool:
212+
"""
213+
Determine whether the submitted job is a continuation of a loop job.
214+
215+
Args:
216+
previous (Informer): Informer associated with the previous job.
217+
218+
Returns:
219+
bool: True if the job is a valid continuation of a previous loop job, False otherwise.
220+
"""
221+
return (
222+
# both the previous job and the current job must be loop jobs
223+
previous.info.loop_info is not None
224+
and self._loop_info is not None
225+
# previous job must be successfully finished
226+
and previous.info.job_state == NaiveState.FINISHED
227+
# the cycle of the current job is one more than the cycle of the previous job
228+
and previous.info.loop_info.current == self._loop_info.current - 1
229+
)
230+
231+
def _continuousJobContinuesLoop(self, previous: Informer) -> bool:
232+
"""
233+
Determine whether the submitted job is a continuation of a continuous job.
234+
235+
Args:
236+
previous (Informer): Informer associated with the previous job.
237+
238+
Returns:
239+
bool: True if the job is a valid continuation of a previous continuous job, False otherwise.
240+
"""
241+
return (
242+
# both the previous and the current job must be continuous jobs
243+
previous.info.job_type == JobType.CONTINUOUS
244+
and self._job_type == JobType.CONTINUOUS
245+
# previous job must be successfully finished
246+
and previous.info.job_state == NaiveState.FINISHED
247+
)
248+
221249
def getInputDir(self) -> Path:
222250
"""
223251
Get path to the job's input directory.
@@ -327,6 +355,9 @@ def _createEnvVarsDict(self) -> dict[str, str]:
327355
env_vars[CFG.env_vars.loop_start] = str(self._loop_info.start)
328356
env_vars[CFG.env_vars.loop_end] = str(self._loop_info.end)
329357
env_vars[CFG.env_vars.archive_format] = self._loop_info.archive_format
358+
359+
# loop job- or continuous job-specific environment variables
360+
if self._job_type in [JobType.LOOP, JobType.CONTINUOUS]:
330361
env_vars[CFG.env_vars.no_resubmit] = str(CFG.exit_codes.qq_run_no_resubmit)
331362

332363
return env_vars

0 commit comments

Comments
 (0)