flowboost 0.2.2__tar.gz

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 (28) hide show
  1. flowboost-0.2.2/PKG-INFO +122 -0
  2. flowboost-0.2.2/README.md +91 -0
  3. flowboost-0.2.2/flowboost/__init__.py +0 -0
  4. flowboost-0.2.2/flowboost/config/config.py +72 -0
  5. flowboost-0.2.2/flowboost/config/optifoam_config.toml +32 -0
  6. flowboost-0.2.2/flowboost/manager/Allrun_wrapper.sh +0 -0
  7. flowboost-0.2.2/flowboost/manager/interfaces/local.py +66 -0
  8. flowboost-0.2.2/flowboost/manager/interfaces/sge.py +79 -0
  9. flowboost-0.2.2/flowboost/manager/interfaces/slurm.py +84 -0
  10. flowboost-0.2.2/flowboost/manager/manager.py +441 -0
  11. flowboost-0.2.2/flowboost/manager/readme.md +14 -0
  12. flowboost-0.2.2/flowboost/openfoam/case.py +701 -0
  13. flowboost-0.2.2/flowboost/openfoam/data.py +322 -0
  14. flowboost-0.2.2/flowboost/openfoam/dictionary.py +651 -0
  15. flowboost-0.2.2/flowboost/openfoam/fields.py +11 -0
  16. flowboost-0.2.2/flowboost/openfoam/interface.py +83 -0
  17. flowboost-0.2.2/flowboost/openfoam/readme.md +68 -0
  18. flowboost-0.2.2/flowboost/openfoam/types.py +234 -0
  19. flowboost-0.2.2/flowboost/optimizer/acquisition_offload.py +85 -0
  20. flowboost-0.2.2/flowboost/optimizer/acquisition_offload.sh +15 -0
  21. flowboost-0.2.2/flowboost/optimizer/backend.py +319 -0
  22. flowboost-0.2.2/flowboost/optimizer/interfaces/Ax.py +524 -0
  23. flowboost-0.2.2/flowboost/optimizer/interfaces/readme.md +4 -0
  24. flowboost-0.2.2/flowboost/optimizer/objectives.py +420 -0
  25. flowboost-0.2.2/flowboost/optimizer/search_space.py +148 -0
  26. flowboost-0.2.2/flowboost/session/session.py +972 -0
  27. flowboost-0.2.2/flowboost/utilities/time.py +40 -0
  28. flowboost-0.2.2/pyproject.toml +67 -0
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: flowboost
3
+ Version: 0.2.2
4
+ Summary: Multi-objective Bayesian optimization library for OpenFOAM
5
+ Keywords: OpenFOAM,CFD,Bayesian optimization,Multi-objective optimization,HPC,Cluster computing
6
+ Author: Daniel Virokannas
7
+ Author-email: Daniel Virokannas <46869890+499602D2@users.noreply.github.com>
8
+ License-Expression: Apache-2.0
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering
15
+ Classifier: Topic :: Scientific/Engineering :: Physics
16
+ Requires-Dist: ax-platform>=1.0
17
+ Requires-Dist: coloredlogs>=15
18
+ Requires-Dist: pandas>=2.2
19
+ Requires-Dist: polars>=1.11
20
+ Requires-Dist: psutil>=5.9
21
+ Requires-Dist: pyarrow>=15
22
+ Requires-Dist: scikit-learn>=1.4
23
+ Requires-Dist: tomlkit>=0.12
24
+ Requires-Dist: polars-lts-cpu>=0.20 ; extra == 'lts-cpu'
25
+ Maintainer: Daniel Virokannas, Bulut Tekgul
26
+ Maintainer-email: Daniel Virokannas <46869890+499602D2@users.noreply.github.com>, Bulut Tekgul <bulut.tekgul@wartsila.com>
27
+ Requires-Python: >=3.10
28
+ Project-URL: Repository, https://github.com/499602D2/flowboost
29
+ Provides-Extra: lts-cpu
30
+ Description-Content-Type: text/markdown
31
+
32
+ # 🏄‍♂️ FlowBoost — Multi-objective Bayesian optimization for OpenFOAM
33
+
34
+ ![Python](https://img.shields.io/badge/python-3.10_%7C_3.11_%7C_3.12_%7C_3.13_%7C_3.14-blue)
35
+
36
+ FlowBoost is a highly configurable and extensible library for handling and optimizing OpenFOAM CFD simulations. It provides ready bindings for state-of-the-art Bayesian optimization using Meta's Ax, powered by PyTorch, and simple interfaces for using any other optimization library.
37
+
38
+ ## Features
39
+ - Easy API syntax (see `examples/`)
40
+ - Ready bindings for [Meta's Ax (Adaptive Experimentation Platform)](https://ax.dev/)
41
+ - Multi-objective, high-dimensional Bayesian optimization
42
+ - SAASBO, GPU acceleration
43
+ - Fully hands-off cluster-native job management
44
+ - Simple interfaces for OpenFOAM cases (`flowboost.Case`)
45
+ - Use any optimization backend by implementing a few interfaces
46
+
47
+ ## Examples
48
+ The `examples/` directory contains code examples for simplified real-world scenarios:
49
+
50
+ 1. `aerofoilNACA0012Steady`: parameter optimization for a NACA 0012 aerofoil steady-state simulation
51
+
52
+ By default, FlowBoost uses Ax's [Service API](https://ax.dev/) as its optimization backend. In practice, any optimizer can be used, as long as it conforms to the abstract `flowboost.optimizer.Backend` base class, which the backend interfaces in `flowboost.optimizer.interfaces` implement.
53
+
54
+ ## OpenFOAM case abstraction
55
+ Working with OpenFOAM cases is performed through the `flowboost.Case` abstraction, which provides a high-level API for OpenFOAM case-data and configuration access. The `Case` abstraction can be used as-is outside of optimization workflows:
56
+
57
+ ```python
58
+ from flowboost import Case
59
+
60
+ # Clone tutorial to current working directory (or a specified dir)
61
+ tutorial_case = Case.from_tutorial("fluid/aerofoilNACA0012Steady")
62
+
63
+ # Dictionary read/write access
64
+ control_dict = tutorial_case.dictionary("system/controlDict")
65
+ control_dict.entry("writeInterval").set("5000")
66
+
67
+ # Access data in an evaluated case
68
+ case = Case("my/case/path")
69
+ df = case.data.simple_function_object_reader("forceCoeffsCompressible")
70
+ ```
71
+
72
+ ## Installation
73
+ FlowBoost requires Python 3.10 or later.
74
+
75
+ ### uv (recommended)
76
+ ```shell
77
+ uv add flowboost
78
+ ```
79
+
80
+ ### pip
81
+ ```shell
82
+ pip install flowboost
83
+ ```
84
+
85
+ ### CPU compatibility
86
+ In order to use the standard `polars` package, your CPU should support AVX2 instructions ([and other SIMD instructions](https://github.com/pola-rs/polars/blob/78dc62851a13b87dc751a627e1e96ba1bf1549ee/py-polars/polars/_cpu_check.py)). These are typically available in Intel Broadwell/4000-series and later, and all AMD Zen-based CPUs.
87
+
88
+ If your CPU is from 2012 or earlier, you will most likely receive an illegal instruction error. This can be solved by installing the `lts-cpu` extra:
89
+
90
+ ```shell
91
+ uv add flowboost[lts-cpu]
92
+ # or: pip install flowboost[lts-cpu]
93
+ ```
94
+
95
+ This installs `polars-lts-cpu`, which is functionally identical but not as performant.
96
+
97
+ ## Development
98
+
99
+ The project is packaged using [uv](https://docs.astral.sh/uv/). The code is linted and formatted using [Ruff](https://docs.astral.sh/ruff/).
100
+
101
+ ```shell
102
+ uv sync
103
+ uv run pytest
104
+ ```
105
+
106
+ ### GPU acceleration
107
+ If your environment has a CUDA-compatible NVIDIA GPU, you should verify you have a recent CUDA Toolkit release. Otherwise, GPU acceleration for PyTorch will not be available. This is especially critical if you are using SAASBO for high-dimensional optimization tasks (≥20 dimensions).
108
+
109
+ ```shell
110
+ nvcc -V
111
+
112
+ # Verify CUDA availability
113
+ python3 -c "import torch; print(torch.cuda.is_available())"
114
+ ```
115
+
116
+ ### Testing
117
+ Passing the full test suite requires OpenFOAM to be installed and sourced. FlowBoost has only been tested using the [openfoam.org](https://openfoam.org/) lineage (not the ESI/openfoam.com fork), specifically version 11.
118
+
119
+ If you wish to contribute code to the project, please ensure your contribution still passes the current test coverage.
120
+
121
+ ## Acknowledgments
122
+ The base functionality for FlowBoost was created as part of a mechanical engineering master's thesis at Aalto University, funded by Wärtsilä. Wärtsilä designs and manufactures marine combustion engines and energy solutions in Vaasa, Finland.
@@ -0,0 +1,91 @@
1
+ # 🏄‍♂️ FlowBoost — Multi-objective Bayesian optimization for OpenFOAM
2
+
3
+ ![Python](https://img.shields.io/badge/python-3.10_%7C_3.11_%7C_3.12_%7C_3.13_%7C_3.14-blue)
4
+
5
+ FlowBoost is a highly configurable and extensible library for handling and optimizing OpenFOAM CFD simulations. It provides ready bindings for state-of-the-art Bayesian optimization using Meta's Ax, powered by PyTorch, and simple interfaces for using any other optimization library.
6
+
7
+ ## Features
8
+ - Easy API syntax (see `examples/`)
9
+ - Ready bindings for [Meta's Ax (Adaptive Experimentation Platform)](https://ax.dev/)
10
+ - Multi-objective, high-dimensional Bayesian optimization
11
+ - SAASBO, GPU acceleration
12
+ - Fully hands-off cluster-native job management
13
+ - Simple interfaces for OpenFOAM cases (`flowboost.Case`)
14
+ - Use any optimization backend by implementing a few interfaces
15
+
16
+ ## Examples
17
+ The `examples/` directory contains code examples for simplified real-world scenarios:
18
+
19
+ 1. `aerofoilNACA0012Steady`: parameter optimization for a NACA 0012 aerofoil steady-state simulation
20
+
21
+ By default, FlowBoost uses Ax's [Service API](https://ax.dev/) as its optimization backend. In practice, any optimizer can be used, as long as it conforms to the abstract `flowboost.optimizer.Backend` base class, which the backend interfaces in `flowboost.optimizer.interfaces` implement.
22
+
23
+ ## OpenFOAM case abstraction
24
+ Working with OpenFOAM cases is performed through the `flowboost.Case` abstraction, which provides a high-level API for OpenFOAM case-data and configuration access. The `Case` abstraction can be used as-is outside of optimization workflows:
25
+
26
+ ```python
27
+ from flowboost import Case
28
+
29
+ # Clone tutorial to current working directory (or a specified dir)
30
+ tutorial_case = Case.from_tutorial("fluid/aerofoilNACA0012Steady")
31
+
32
+ # Dictionary read/write access
33
+ control_dict = tutorial_case.dictionary("system/controlDict")
34
+ control_dict.entry("writeInterval").set("5000")
35
+
36
+ # Access data in an evaluated case
37
+ case = Case("my/case/path")
38
+ df = case.data.simple_function_object_reader("forceCoeffsCompressible")
39
+ ```
40
+
41
+ ## Installation
42
+ FlowBoost requires Python 3.10 or later.
43
+
44
+ ### uv (recommended)
45
+ ```shell
46
+ uv add flowboost
47
+ ```
48
+
49
+ ### pip
50
+ ```shell
51
+ pip install flowboost
52
+ ```
53
+
54
+ ### CPU compatibility
55
+ In order to use the standard `polars` package, your CPU should support AVX2 instructions ([and other SIMD instructions](https://github.com/pola-rs/polars/blob/78dc62851a13b87dc751a627e1e96ba1bf1549ee/py-polars/polars/_cpu_check.py)). These are typically available in Intel Broadwell/4000-series and later, and all AMD Zen-based CPUs.
56
+
57
+ If your CPU is from 2012 or earlier, you will most likely receive an illegal instruction error. This can be solved by installing the `lts-cpu` extra:
58
+
59
+ ```shell
60
+ uv add flowboost[lts-cpu]
61
+ # or: pip install flowboost[lts-cpu]
62
+ ```
63
+
64
+ This installs `polars-lts-cpu`, which is functionally identical but not as performant.
65
+
66
+ ## Development
67
+
68
+ The project is packaged using [uv](https://docs.astral.sh/uv/). The code is linted and formatted using [Ruff](https://docs.astral.sh/ruff/).
69
+
70
+ ```shell
71
+ uv sync
72
+ uv run pytest
73
+ ```
74
+
75
+ ### GPU acceleration
76
+ If your environment has a CUDA-compatible NVIDIA GPU, you should verify you have a recent CUDA Toolkit release. Otherwise, GPU acceleration for PyTorch will not be available. This is especially critical if you are using SAASBO for high-dimensional optimization tasks (≥20 dimensions).
77
+
78
+ ```shell
79
+ nvcc -V
80
+
81
+ # Verify CUDA availability
82
+ python3 -c "import torch; print(torch.cuda.is_available())"
83
+ ```
84
+
85
+ ### Testing
86
+ Passing the full test suite requires OpenFOAM to be installed and sourced. FlowBoost has only been tested using the [openfoam.org](https://openfoam.org/) lineage (not the ESI/openfoam.com fork), specifically version 11.
87
+
88
+ If you wish to contribute code to the project, please ensure your contribution still passes the current test coverage.
89
+
90
+ ## Acknowledgments
91
+ The base functionality for FlowBoost was created as part of a mechanical engineering master's thesis at Aalto University, funded by Wärtsilä. Wärtsilä designs and manufactures marine combustion engines and energy solutions in Vaasa, Finland.
File without changes
@@ -0,0 +1,72 @@
1
+ import importlib.resources as pkg_resources
2
+ import logging
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ import tomlkit
7
+
8
+ DEFAULT_CONFIG_NAME: str = "flowboost_config.toml"
9
+
10
+
11
+ def validate(config: dict) -> bool:
12
+ """Validate the TOML configuration file.
13
+
14
+ Args:
15
+ config_path (Path): The path to the TOML configuration file.
16
+
17
+ Returns:
18
+ bool: True if the configuration is valid, False otherwise.
19
+ """
20
+ # Check for offload_acquisition
21
+ offload_acquisition = config.get("scheduler", {}).get("offload_acquisition", False)
22
+
23
+ if offload_acquisition:
24
+ # Ensure acquisition configuration is present and valid
25
+ if "scheduler.acquisition" not in config:
26
+ logging.error(
27
+ "Offload acquisition is enabled, but `[scheduler.acquisition]` not configured"
28
+ )
29
+ return False
30
+
31
+ return True
32
+
33
+
34
+ def create(dir: Path, filename: str = DEFAULT_CONFIG_NAME) -> dict:
35
+ with pkg_resources.path("flowboost.config", DEFAULT_CONFIG_NAME) as config_path:
36
+ if not config_path.exists():
37
+ raise FileNotFoundError(f"Configuration template not found [{config_path}]")
38
+
39
+ dest = Path(dir, filename)
40
+ if dest.exists():
41
+ raise FileExistsError("Configuration already exists, but create() called")
42
+
43
+ shutil.copy(config_path, dest)
44
+ with open(dest, "r") as config_file:
45
+ config = tomlkit.load(config_file)
46
+
47
+ if not validate(config):
48
+ raise ValueError("Default configuration is invalid")
49
+
50
+ return config
51
+
52
+
53
+ def load(dir: Path, filename: str = DEFAULT_CONFIG_NAME) -> dict:
54
+ fpath = Path(dir, filename)
55
+
56
+ if not fpath.exists():
57
+ return create(dir=dir, filename=filename)
58
+
59
+ with open(fpath, "r") as toml_f:
60
+ config = tomlkit.load(toml_f)
61
+
62
+ if not validate(config):
63
+ raise ValueError("Configuration is invalid")
64
+
65
+ return config
66
+
67
+
68
+ def save(config: dict, dir: Path, filename: str = DEFAULT_CONFIG_NAME):
69
+ fpath = Path(dir, filename)
70
+
71
+ with open(fpath, "w") as toml_f:
72
+ tomlkit.dump(config, toml_f)
@@ -0,0 +1,32 @@
1
+ [session]
2
+ name = ""
3
+ data_dir = ""
4
+ archival_dir = ""
5
+ dataframe_format = "polars"
6
+ created_at = ""
7
+
8
+ [template]
9
+ path = ""
10
+ additional_files = []
11
+
12
+ [optimizer]
13
+ type = "Ax"
14
+ offload_acquisition = false # Acquisition offloading in cluster env
15
+
16
+ [scheduler]
17
+ type = "" # Manager-implementing class name
18
+ job_limit = 1 # Also node reservation limit (TODO not always)
19
+
20
+ [scheduler.OpenFOAM]
21
+ # Can also be provided in your template case's Allrun script
22
+ # args = { pe = "orte 36", M = "user@example.com", m = "base" }
23
+ # setup = [
24
+ # "module load gnu openmpi",
25
+ # "source /nfs/prg/OpenFOAM/OpenFOAM-dev/etc/bashrc",
26
+ # ]
27
+
28
+ [scheduler.acquisition]
29
+ # This is a required configuration if you desire offloaded acquisition
30
+ # args = { q = "gpgpu", M = "user@example.com", m = "base" }
31
+ # setup = ["source /nfs/prg/anaconda3/bin/activate", "conda activate py3.10"]
32
+ # torch_device = ""
File without changes
@@ -0,0 +1,66 @@
1
+ import logging
2
+ import os
3
+ import signal
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import Any, Optional
7
+
8
+ import psutil
9
+
10
+ from flowboost.manager.manager import Manager
11
+ from flowboost.openfoam.interface import FOAM
12
+
13
+
14
+ class Local(Manager):
15
+ def __init__(self, wdir: Path | str, job_limit: int = 1) -> None:
16
+ super().__init__(wdir, job_limit)
17
+ self.shell: str = "bash" # os.getenv("SHELL", "bash")
18
+
19
+ @staticmethod
20
+ def _is_available() -> bool:
21
+ available = FOAM.in_env()
22
+ if not available:
23
+ logging.error("OpenFOAM not found in PATH")
24
+
25
+ return available
26
+
27
+ def _submit_job(
28
+ self,
29
+ job_name: str,
30
+ submission_cwd: Path,
31
+ script: Path,
32
+ script_args: dict[str, Any] = {},
33
+ ) -> Optional[str]:
34
+ # Base command
35
+ cmd = [self.shell, script]
36
+
37
+ if script_args:
38
+ script_kv = Manager._construct_scipt_args(script_args, " ")
39
+ cmd.extend(script_kv)
40
+
41
+ # Execute the script and get the PID
42
+ process = subprocess.Popen(cmd, cwd=submission_cwd, start_new_session=True)
43
+ pid = process.pid
44
+
45
+ # Create and track the job
46
+ return str(pid)
47
+
48
+ def _cancel_job(self, job_id: str) -> bool:
49
+ try:
50
+ os.kill(int(job_id), signal.SIGTERM)
51
+ except OSError:
52
+ return False
53
+
54
+ return True
55
+
56
+ def _job_has_finished(self, job_id: str) -> bool:
57
+ try:
58
+ # Check if the process is still running
59
+ process = psutil.Process(int(job_id))
60
+ # If the process is running or sleeping, it's not finished
61
+ if process.status() in [psutil.STATUS_RUNNING, psutil.STATUS_SLEEPING]:
62
+ return False
63
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
64
+ return True
65
+
66
+ return True
@@ -0,0 +1,79 @@
1
+ import logging
2
+ import shutil
3
+ import subprocess
4
+ from pathlib import Path
5
+ from typing import Any, Optional
6
+
7
+ from flowboost.manager.manager import Manager
8
+
9
+
10
+ class SGE(Manager):
11
+ def __init__(self, wdir: Path | str, job_limit: int) -> None:
12
+ super().__init__(wdir, job_limit)
13
+
14
+ @staticmethod
15
+ def _is_available() -> bool:
16
+ if shutil.which("qsub") and shutil.which("qstat"):
17
+ return True
18
+
19
+ logging.error("qsub or qstat commands not found in PATH")
20
+ return False
21
+
22
+ def _submit_job(
23
+ self,
24
+ job_name: str,
25
+ submission_cwd: Path,
26
+ script: Path,
27
+ script_args: dict[str, Any] = {},
28
+ ) -> Optional[str]:
29
+ # Base command with name
30
+ cmd = ["qsub", "-N", job_name]
31
+
32
+ if script_args:
33
+ # If Allrun accepts args, pass them
34
+ script_kv = Manager._construct_scipt_args(script_args)
35
+ cmd.extend(["-v", script_kv])
36
+
37
+ cmd.append(str(script))
38
+
39
+ # Run in case working directory
40
+ result = subprocess.run(cmd, capture_output=True, text=True, cwd=submission_cwd)
41
+
42
+ if result.returncode != 0:
43
+ logging.error(f"Error submitting job: out='{result.stderr.strip()}'")
44
+ return None
45
+
46
+ job_id = result.stdout.split()[2].split(".")[0]
47
+ return job_id
48
+
49
+ def _cancel_job(self, job_id: str) -> bool:
50
+ cmd = ["qdel", job_id]
51
+ result = subprocess.run(cmd, capture_output=True, text=True)
52
+
53
+ if result.returncode != 0:
54
+ logging.error(f"Error cancelling jobs: {result.stderr.strip()}")
55
+ return False
56
+
57
+ return True
58
+
59
+ def _job_has_finished(self, job_id: str) -> bool:
60
+ cmd = ["qstat", "-j", job_id]
61
+ result = subprocess.run(cmd, capture_output=True, text=True)
62
+
63
+ if result.returncode == 1:
64
+ if "Following jobs do not exist" in result.stderr:
65
+ # Job does not exist, meaning it has ended: OK
66
+ return True
67
+
68
+ logging.warning(
69
+ f"Querying job finish status failed: {result.stderr.strip()}"
70
+ )
71
+ return False
72
+
73
+ return False
74
+
75
+ def _get_job_info(self, job_id: str) -> str:
76
+ """Fetch job details from Sun Grid Engine using the given job_id."""
77
+ cmd = ["qstat", "-j", job_id]
78
+ result = subprocess.run(cmd, capture_output=True, text=True)
79
+ return result.stdout
@@ -0,0 +1,84 @@
1
+ import logging
2
+ import shutil
3
+ import subprocess
4
+ from pathlib import Path
5
+ from typing import Any, Optional
6
+
7
+ from flowboost.manager.manager import Manager
8
+
9
+ class Slurm(Manager):
10
+ def __init__(self, wdir: Path | str, job_limit: int) -> None:
11
+ super().__init__(wdir, job_limit)
12
+
13
+ @staticmethod
14
+ def _is_available() -> bool:
15
+ if shutil.which("sbatch") and shutil.which("squeue"):
16
+ return True
17
+
18
+ logging.error("sbatch or squeue commands not found in PATH")
19
+ return False
20
+
21
+ def _submit_job(
22
+ self,
23
+ job_name: str,
24
+ submission_cwd: Path,
25
+ script: Path,
26
+ script_args: dict[str, Any] = {},
27
+ ) -> Optional[str]:
28
+ # Base command with name
29
+ cmd = ["sbatch", "--job-name", job_name]
30
+
31
+ if script_args:
32
+ # If Allrun accepts args, pass them
33
+ script_kv = Manager._construct_scipt_args(script_args)
34
+ cmd.extend(["-v", script_kv])
35
+
36
+ cmd.append(str(script))
37
+ # Run in case working directory
38
+ result = subprocess.run(cmd, capture_output=True, text=True, cwd=submission_cwd)
39
+
40
+ if result.returncode != 0:
41
+ logging.error(f"Error submitting job: out='{result.stderr.strip()}'")
42
+ return None
43
+
44
+ job_id = result.stdout.split()[-1]
45
+ return job_id
46
+
47
+
48
+ def _cancel_job(self, job_id: str) -> bool:
49
+ cmd = ["scancel", job_id]
50
+ result = subprocess.run(cmd, capture_output=True, text=True)
51
+
52
+ if result.returncode != 0:
53
+ logging.error(f"Error cancelling jobs: {result.stderr.strip()}")
54
+ return False
55
+
56
+ return True
57
+
58
+ def _job_has_finished(self, job_id: str) -> bool:
59
+ """Check if a Slurm job has finished."""
60
+ cmd = ["squeue", "-j", job_id, "-h"] # -h suppresses header
61
+ result = subprocess.run(cmd, capture_output=True, text=True)
62
+
63
+ if result.returncode != 0:
64
+ # Job not in queue - likely finished or never existed
65
+ if "Invalid job id specified" in result.stderr:
66
+ return True
67
+
68
+ logging.warning(
69
+ f"Querying job finish status failed: {result.stderr.strip()}"
70
+ )
71
+ return False
72
+
73
+ # If squeue returns successfully with output, job is still running/pending
74
+ if result.stdout.strip():
75
+ return False
76
+
77
+ # Empty output means job finished
78
+ return True
79
+
80
+ def _get_job_info(self, job_id: str) -> str:
81
+ """Fetch job details from Slurm using the given job_id."""
82
+ cmd = ["scontrol", "show", "job", job_id]
83
+ result = subprocess.run(cmd, capture_output=True, text=True)
84
+ return result.stdout