Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions docs/further.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,14 @@ The following limits can be defined for each partition:

| Parameter | Type | Description | Default |
| ----------------------- | --------- | ---------------------------------- | --------- |
| `max_runtime` | int | Maximum walltime in minutes | unlimited |
| `max_runtime` | int/str | Maximum walltime | unlimited |
| `max_mem_mb` | int | Maximum total memory in MB | unlimited |
| `max_mem_mb_per_cpu` | int | Maximum memory per CPU in MB | unlimited |
| `max_cpus_per_task` | int | Maximum CPUs per task | unlimited |
| `max_nodes` | int | Maximum number of nodes | unlimited |
| `max_tasks` | int | Maximum number of tasks | unlimited |
| `max_tasks_per_node` | int | Maximum tasks per node | unlimited |
| `max_threads` | int | Maximum threads per node | unlimited |
| `max_threads` | int | Maximum threads per node | unlimited |
| `max_gpu` | int | Maximum number of GPUs | 0 |
| `available_gpu_models` | list[str] | List of available GPU models | none |
| `max_cpus_per_gpu` | int | Maximum CPUs per GPU | unlimited |
Expand All @@ -103,6 +103,19 @@ The following limits can be defined for each partition:
| `available_constraints` | list[str] | List of available node constraints | none |
| `cluster` | str | Cluster name in multi-cluster setup | none |

Note: the `max_runtime` definition may contain
- Numeric values (assumed to be in minutes): 120, 120.5
- Snakemake-style time strings: "6d", "12h", "30m", "90s", "2d12h30m"
- SLURM time formats:
- "minutes" (e.g., "60")
- "minutes:seconds" (interpreted as hours:minutes, e.g., "60:30")
- "hours:minutes:seconds" (e.g., "1:30:45")
- "days-hours" (e.g., "2-12")
- "days-hours:minutes" (e.g., "2-12:30")
- "days-hours:minutes:seconds" (e.g., "2-12:30:45")

They are all auto-converted to minutes. Seconds are rounded to the nearest value in minutes.

##### Example Partition Configuration

```yaml
Expand Down
10 changes: 10 additions & 0 deletions snakemake_executor_plugin_slurm/partitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
JobExecutorInterface,
)
from snakemake_interface_executor_plugins.logging import LoggerExecutorInterface
from .utils import parse_time_to_minutes


def read_partition_file(partition_file: Path) -> List["Partition"]:
Expand Down Expand Up @@ -222,6 +223,15 @@ class PartitionLimits:
# Node features/constraints
available_constraints: Optional[List[str]] = None

def __post_init__(self):
"""Convert max_runtime to minutes if specified as a time string"""
# Check if max_runtime is a string or needs conversion
# isinf() only works on numeric types, so check type first
if isinstance(self.max_runtime, str) or (
isinstance(self.max_runtime, (int, float)) and not isinf(self.max_runtime)
):
self.max_runtime = parse_time_to_minutes(self.max_runtime)


@dataclass
class Partition:
Expand Down
121 changes: 121 additions & 0 deletions snakemake_executor_plugin_slurm/utils.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,136 @@
# utility functions for the SLURM executor plugin

import math
import os
import re
from pathlib import Path
from typing import Union

from snakemake_interface_executor_plugins.jobs import (
JobExecutorInterface,
)
from snakemake_interface_common.exceptions import WorkflowError


def round_half_up(n):
return int(math.floor(n + 0.5))


def parse_time_to_minutes(time_value: Union[str, int, float]) -> int:
"""
Convert a time specification to minutes (integer). This function
is intended to handle the partition definitions for the max_runtime
value in a partition config file.
Supports:
- Numeric values (assumed to be in minutes): 120, 120.5
- Snakemake-style time strings: "6d", "12h", "30m", "90s", "2d12h30m"
- SLURM time formats:
- "minutes" (e.g., "60")
- "minutes:seconds" (interpreted as hours:minutes, e.g., "60:30")
- "hours:minutes:seconds" (e.g., "1:30:45")
- "days-hours" (e.g., "2-12")
- "days-hours:minutes" (e.g., "2-12:30")
- "days-hours:minutes:seconds" (e.g., "2-12:30:45")
Args:
time_value: Time specification as string, int, or float
Returns:
Time in minutes as integer (fractional minutes are rounded)
Raises:
WorkflowError: If the time format is invalid
"""
# If already numeric, return as integer minutes (rounded)
if isinstance(time_value, (int, float)):
return round_half_up(time_value) # implicit conversion to int

# Convert to string and strip whitespace
time_str = str(time_value).strip()

# Try to parse as plain number first
try:
return round_half_up(float(time_str)) # implicit conversion to int
except ValueError:
pass

# Try SLURM time formats first (with colons and dashes)
# Format: days-hours:minutes:seconds or variations
if "-" in time_str or ":" in time_str:
try:
days = 0
hours = 0
minutes = 0
seconds = 0

# Split by dash first (days separator)
if "-" in time_str:
parts = time_str.split("-")
if len(parts) != 2:
raise ValueError("Invalid format with dash")
days = int(parts[0])
time_str = parts[1]

# Split by colon (time separator)
time_parts = time_str.split(":")

if len(time_parts) == 1:
# Just hours (after dash) or just minutes
if days > 0:
hours = int(time_parts[0])
else:
minutes = int(time_parts[1])
elif len(time_parts) == 2:
# was: days-hours:minutes
hours = int(time_parts[0])
minutes = int(time_parts[1])
elif len(time_parts) == 3:
# was: hours:minutes:seconds
hours = int(time_parts[0])
minutes = int(time_parts[1])
seconds = int(time_parts[2])
else:
raise ValueError("Too many colons in time format")

# Convert everything to minutes
total_minutes = days * 24 * 60 + hours * 60 + minutes + seconds / 60.0
return round_half_up(total_minutes) # implicit conversion to int

except (ValueError, IndexError):
# If SLURM format parsing fails, try Snakemake style below
pass

# Parse Snakemake-style time strings (e.g., "6d", "12h", "30m", "90s", "2d12h30m")
# Pattern matches: optional number followed by unit (d, h, m, s)
pattern = r"(\d+(?:\.\d+)?)\s*([dhms])"
matches = re.findall(pattern, time_str.lower())

if not matches:
raise WorkflowError(
f"Invalid time format: '{time_value}'. "
f"Expected formats:\n"
f" - Numeric value in minutes: 120\n"
f" - Snakemake style: '6d', '12h', '30m', '90s', '2d12h30m'\n"
f" - SLURM style: 'minutes', 'minutes:seconds', 'hours:minutes:seconds',\n"
f" 'days-hours', 'days-hours:minutes', 'days-hours:minutes:seconds'"
)

total_minutes = 0.0
for value, unit in matches:
num = float(value)
if unit == "d":
total_minutes += num * 24 * 60
elif unit == "h":
total_minutes += num * 60
elif unit == "m":
total_minutes += num
elif unit == "s":
total_minutes += num / 60

return round_half_up(total_minutes)


def delete_slurm_environment():
"""
Function to delete all environment variables
Expand Down
Loading