Skip to content

Commit 45482da

Browse files
committed
✨ New DatcmpJob
xtl.config.settings:DependenciesSettings - Added new dependency for ATSAS xtl.saxs.jobs.atsas_utils:DatcmpOptions - Configuration for ATSAS datcmp xtl.saxs.jobs.atsas:DatcmpJob - Job for ATSAS datcmp xtl.automate.batchfile:BatchFile - Fixed a bug in .add_commands() where if a single command was passed as a string it would not have been properly propagated - Fixed a bug in .dependencies where the missing dependencies where not properly reported xtl.jobs.config:BatchConfig - Added .dependencies attribute - Updated the .get_batch() method to propagate the dependencies to the BatchFile constructor - Updated the .shell setter to also accept WslShell instances
1 parent a0f068b commit 45482da

File tree

8 files changed

+183
-27
lines changed

8 files changed

+183
-27
lines changed

src/xtl/automate/batchfile.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,10 @@ def dependencies(self) -> list['DependencySettings']:
108108
for dname, dep in settings.dependencies:
109109
if dname in self._dependencies:
110110
deps.append(dep)
111-
else:
111+
112+
# Report missing dependencies
113+
for dname in self._dependencies:
114+
if dname not in settings.dependencies.to_dict():
112115
logger.warning('Dependency %(name)s not found in xtl.settings, '
113116
'skipping', {'name': dname})
114117
return deps
@@ -148,6 +151,8 @@ def add_commands(self, *commands: str | Iterable[str]) -> None:
148151
"""
149152
if len(commands) == 1 and isinstance(commands[0], Iterable): # unpack a list or tuple of commands
150153
commands = commands[0]
154+
if isinstance(commands, str):
155+
commands = [commands]
151156
for command in commands:
152157
if not isinstance(command, str):
153158
raise TypeError(f'\'command\' must be a str, not {type(command)}')

src/xtl/config/settings.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ class DependenciesSettings(Settings):
126126
"""
127127

128128
# Model attributes
129+
atsas: DependencySettings = \
130+
Option(
131+
default=DependencySettings(
132+
provides={'datcmp'}
133+
)
134+
)
135+
129136
autoproc: DependencySettings = \
130137
Option(
131138
default=DependencySettings(

src/xtl/jobs/config.py

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
from pydantic import computed_field, PrivateAttr, model_validator
77

88
from xtl import Logger
9-
from xtl.automate.shells import Shell, DefaultShell
10-
from xtl.automate.sites import ComputeSiteType, LocalSite, BiotixHPC
9+
from xtl.automate.shells import Shell, DefaultShell, WslShell
10+
from xtl.automate.sites import ComputeSiteType, LocalSite
1111
from xtl.common.options import Option, Options
1212
from xtl.common.os import FilePermissions
1313
from xtl.common.serializers import PermissionOctal
@@ -23,24 +23,43 @@ class BatchConfig(Options):
2323
"""
2424
Configuration for execution of batch files.
2525
"""
26-
filename: str = Option(default='batch_job', desc='Batch file name (without '
27-
'extension)')
28-
directory: Path | str = Option(default_factory=lambda: Path(tempfile.gettempdir()),
29-
desc='Directory for dumping batch file and logs',
30-
cast_as=Path)
31-
permissions: FilePermissions | str | int = Option(default=FilePermissions(0o700),
32-
desc='Permissions for the batch '
33-
'file in octal format '
34-
'(e.g., 700)',
35-
cast_as=FilePermissions,
36-
formatter=PermissionOctal)
37-
compute_site: ComputeSiteType = Option(default=LocalSite(), desc='Compute site')
38-
default_shell: Optional[Shell] = Option(default=None,
39-
desc='Default shell to use for batch '
40-
'execution.')
41-
compatible_shells: set[Shell] = Option(default_factory=set,
42-
desc='List of compatible shell types for '
43-
'this batch job')
26+
filename: str = \
27+
Option(
28+
default='batch_job',
29+
desc='Batch file name (without extension)'
30+
)
31+
directory: Path = \
32+
Option(
33+
default_factory=lambda: Path(tempfile.mkdtemp()),
34+
desc='Directory for dumping batch file and logs'
35+
)
36+
permissions: FilePermissions | str | int = \
37+
Option(
38+
default=FilePermissions(0o700),
39+
desc='Permissions for the batch file in octal format (e.g., 700)',
40+
cast_as=FilePermissions,
41+
formatter=PermissionOctal
42+
)
43+
compute_site: ComputeSiteType = \
44+
Option(
45+
default=LocalSite(),
46+
desc='Compute site'
47+
)
48+
default_shell: Optional[Shell] = \
49+
Option(
50+
default=None,
51+
desc='Default shell to use for batch execution.'
52+
)
53+
compatible_shells: set[Shell] = \
54+
Option(
55+
default_factory=set,
56+
desc='List of compatible shell types for this batch job'
57+
)
58+
dependencies: set[str] = \
59+
Option(
60+
default_factory=set,
61+
desc='List of dependencies required for this batch job'
62+
)
4463

4564
_shell: Shell | None = PrivateAttr(None)
4665

@@ -104,6 +123,7 @@ def stderr(self) -> Path:
104123
return self.directory / f
105124

106125
@computed_field
126+
@property
107127
def shell(self) -> Shell:
108128
"""
109129
Returns the shell that will be used to execute the batch file.
@@ -115,8 +135,8 @@ def shell(self, value: Shell):
115135
"""
116136
Sets the shell to be used for executing the batch file.
117137
"""
118-
if not isinstance(value, Shell):
119-
raise ValueError(f'Shell must be an instance of {Shell.__name__}')
138+
if not isinstance(value, (Shell, WslShell)):
139+
raise ValueError(f'shell must be an instance of {Shell.__name__}')
120140
self._shell = value
121141

122142
def get_batch(self) -> 'BatchFile':
@@ -126,7 +146,8 @@ def get_batch(self) -> 'BatchFile':
126146
from xtl.automate.batchfile import BatchFile
127147
batch = BatchFile(filename=self.directory/self.filename,
128148
compute_site=self.compute_site,
129-
shell=self.shell)
149+
shell=self.shell,
150+
dependencies=self.dependencies)
130151
batch.permissions = self.permissions
131152
return batch
132153

src/xtl/jobs/jobs.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,10 @@ def get_logger(cls, job_id: str, config: LoggerConfig = None) -> logging.Logger:
340340

341341
return logger
342342

343-
async def _execute_batch(self, commands: list[str], args: list[str] = None,
344-
filename: str = None, stdout_log: Path = None,
345-
stderr_log: Path = None) -> JobResults:
343+
async def _execute_batch(self, commands: str | Iterable[str],
344+
args: Iterable[str] = None, filename: str = None,
345+
stdout_log: Path = None, stderr_log: Path = None) -> \
346+
JobResults:
346347
"""
347348
Execute a batch file with the specified commands.
348349

src/xtl/saxs/__init__.py

Whitespace-only changes.

src/xtl/saxs/jobs/__init__.py

Whitespace-only changes.

src/xtl/saxs/jobs/atsas.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import abc
2+
from pathlib import Path
3+
4+
from xtl.common.options import Option
5+
from xtl.jobs.jobs import Job
6+
from xtl.jobs.config import JobConfig
7+
from xtl.saxs.jobs.atsas_utils import ATSASOptions, DatcmpOptions
8+
9+
10+
class ATSASJobConfig(JobConfig, abc.ABC):
11+
"""
12+
Base configuration for ATSAS jobs.
13+
"""
14+
options: ATSASOptions
15+
16+
@abc.abstractmethod
17+
def get_command(self, as_list: bool = False) -> list[str] | str:
18+
...
19+
20+
21+
class DatcmpJobConfig(ATSASJobConfig):
22+
files: list[Path] = \
23+
Option(
24+
...,
25+
desc='List of data files to compare',
26+
min_length=2,
27+
path_exists=True)
28+
options: DatcmpOptions = \
29+
Option(
30+
default_factory=DatcmpOptions,
31+
desc='Options for `datcmp`'
32+
)
33+
34+
def get_command(self, as_list: bool = False) -> list[str] | str:
35+
parts = list(map(str, [self.options.executable,
36+
*self.files,
37+
*self.options.get_args()]))
38+
if as_list:
39+
return parts
40+
return ' '.join(parts)
41+
42+
43+
class DatcmpJob(Job[DatcmpJobConfig]):
44+
"""
45+
Job to compare SAXS datasets using ATSAS datcmp.
46+
"""
47+
48+
async def _execute(self):
49+
if self.config is None:
50+
self.logger.error('Job is not configured.')
51+
raise ValueError('Job is not configured.')
52+
53+
# Execute the command
54+
results = await self._execute_batch(self.config.get_command())
55+
56+
if not results.success or 'stdout' not in results.data:
57+
self.logger.error('An error occurred during execution of the batch file %s',
58+
results.error)
59+
return results
60+
61+
return results.data['stdout']

src/xtl/saxs/jobs/atsas_utils.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from pydantic import PrivateAttr
2+
3+
from xtl.common.options import Option, Options
4+
5+
6+
class ATSASOptions(Options):
7+
"""
8+
Base class for ATSAS job options.
9+
"""
10+
_executable: str = PrivateAttr()
11+
12+
@property
13+
def executable(self) -> str:
14+
return self._executable
15+
16+
def get_args(self) -> list[str]:
17+
"""
18+
Returns the command-line arguments for the ATSAS executable.
19+
"""
20+
args = []
21+
for key, value in self.to_dict(by_alias=True).items():
22+
if value is not None:
23+
args.append(f'--{key}={value}')
24+
return args
25+
26+
27+
class DatcmpOptions(ATSASOptions):
28+
"""
29+
Configuration for an ATSAS datcmp job.
30+
"""
31+
_executable: str = PrivateAttr(default='datcmp')
32+
33+
mode: str | None = \
34+
Option(
35+
default='PAIRWISE',
36+
desc='Comparison mode',
37+
choices={'PAIRWISE', 'INDEPENDENT', None}
38+
)
39+
test: str | None = \
40+
Option(
41+
default='CORMAP',
42+
desc='Test name',
43+
choices={'CORMAP', 'CHI-SQUARE', 'ANDERSON-DARLING', None}
44+
)
45+
adjust: str | None = \
46+
Option(
47+
default='FWER',
48+
desc='Adjustment for multiple testing',
49+
choices={'FWER', 'FDR', None}
50+
)
51+
alpha: float | None = \
52+
Option(
53+
default=0.01,
54+
desc='Significance level for clique search'
55+
)
56+
format: str | None = \
57+
Option(
58+
default='FULL',
59+
desc='Output format',
60+
choices={'FULL', 'CSV', None}
61+
)

0 commit comments

Comments
 (0)