Skip to content

Commit

Permalink
Merge pull request #935 from haddocking/934-less-io-incompatible-with…
Browse files Browse the repository at this point in the history
…-batch-mode

Add logic to check for incompatible parameters
  • Loading branch information
rvhonorato authored Aug 21, 2024
2 parents d71e541 + 1d58c24 commit 7a9434e
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 144 deletions.
30 changes: 30 additions & 0 deletions src/haddock/gear/prepare_run.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Logic pertraining to preparing the run files and folders."""

import difflib
import importlib
import itertools as it
Expand Down Expand Up @@ -76,6 +77,7 @@
modules_category,
modules_names,
non_mandatory_general_parameters_defaults,
incompatible_defaults_params,
)
from haddock.modules.analysis import (
confirm_resdic_chainid_length,
Expand Down Expand Up @@ -269,6 +271,8 @@ def setup_run(
reference_parameters=ALL_POSSIBLE_GENERAL_PARAMETERS,
)

validate_parameters_are_not_incompatible(general_params)

# --extend-run configs do not define the run directory
# in the config file. So we take it from the argument.
if not_none(extend_run):
Expand Down Expand Up @@ -940,6 +944,32 @@ def validate_module_names_are_not_misspelled(params: ParamMap) -> None:
return


def validate_parameters_are_not_incompatible(params: ParamMap) -> None:
"""
Validate parameters are not incompatible.
Parameters
----------
params : ParamMap
A mapping of parameter names to their values.
Raises
------
ValueError
If any parameter in `params` is incompatible with another parameter as defined by `incompatible_params`.
"""
for limiting_param, incompatibilities in incompatible_defaults_params.items():
# Check if the limiting parameter is present in the parameters
if limiting_param in params:
# Check each incompatibility for the limiting parameter
for incompatible_param, incompatible_value in incompatibilities.items():
# Check if the incompatible parameter is present and has the incompatible value
if params.get(incompatible_param) == incompatible_value:
raise ValueError(
f"Parameter `{limiting_param}` is incompatible with `{incompatible_param}={incompatible_value}`."
)


def validate_parameters_are_not_misspelled(
params: Iterable[str], reference_parameters: Iterable[str]
) -> None:
Expand Down
30 changes: 30 additions & 0 deletions src/haddock/gear/yaml2cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
function names. In these cases, we always mean the HADDOCK3 YAML
configuration files which have specific keys.
"""

import os
from collections.abc import Mapping
from pathlib import Path
Expand Down Expand Up @@ -214,3 +215,32 @@ def flat_yaml_cfg(cfg: ParamMap) -> ParamDict:
new[param] = new_value

return new


def find_incompatible_parameters(yaml_file: Path) -> dict[str, ParamDict]:
"""
Reads a YAML configuration file and identifies nodes containing the 'incompatible' key.
This function takes the path to a YAML file, reads its contents, and searches for nodes
that include an 'incompatible' key. It returns a dictionary where each key is a node name
and each value is another dictionary representing the 'incompatible' parameters for that node.
Parameters
----------
yaml_file : Path
The path to the YAML file to be read.
Return
------
incompatible_parameters : dict[str, dict[str, str]]
A dictionary with node names as keys and their corresponding
'incompatible' parameters as values.
"""
config = read_from_yaml(yaml_file=yaml_file)

# Go over each node and see which contains the `incompatible` key
incompatible_parameters = {}
for node, values in config.items():
if "incompatible" in values:
incompatible_parameters[node] = values["incompatible"]
return incompatible_parameters
90 changes: 45 additions & 45 deletions src/haddock/modules/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""HADDOCK3 modules."""

import re
from abc import ABC, abstractmethod
from contextlib import contextmanager, suppress
Expand All @@ -18,11 +19,11 @@
Optional,
ParamDict,
Union,
)
)
from haddock.gear import config
from haddock.gear.clean_steps import clean_output
from haddock.gear.parameters import config_mandatory_general_parameters
from haddock.gear.yaml2cfg import read_from_yaml_config
from haddock.gear.yaml2cfg import read_from_yaml_config, find_incompatible_parameters
from haddock.libs.libhpc import HPCScheduler
from haddock.libs.libio import folder_exists, working_directory
from haddock.libs.libmpi import MPIScheduler
Expand All @@ -39,7 +40,7 @@
module.name: category.name
for category in modules_folder.glob(_folder_match_regex)
for module in category.glob(_folder_match_regex)
}
}
"""Indexes each module in its specific category. Keys are Paths to the module,
values are their categories. Categories are the modules parent folders."""

Expand All @@ -52,25 +53,28 @@
"scoring",
"analysis",
"extras",
]
]

# this dictionary defines non-mandatory general parameters that can be defined
# as global parameters thus affect all modules, or, instead, can be defined per
# module where the module definition overwrites global definition. Not all
# modules will use these parameters. It is the responsibility of the module to
# extract the parameters it needs.
# the config file is in modules/defaults.cfg
non_mandatory_general_parameters_defaults = read_from_yaml_config(modules_defaults_path) # noqa : E501
non_mandatory_general_parameters_defaults = read_from_yaml_config(
modules_defaults_path
) # noqa : E501

incompatible_defaults_params = find_incompatible_parameters(modules_defaults_path)

config_readers = {
".yaml": read_from_yaml_config,
".cfg": config.load,
}
}

_step_folder_regex = tuple(
r"[0-9]+_" + mod_name
for mod_name in modules_category.keys()
)
r"[0-9]+_" + mod_name for mod_name in modules_category.keys()
)
step_folder_regex = "(" + "|".join(_step_folder_regex) + ")"
"""
String for regular expression to match module folders in a run directory.
Expand Down Expand Up @@ -100,7 +104,7 @@ def _not_valid_config() -> Generator[None, None, None]:
emsg = (
"The configuration file extension is not supported. "
f"Supported types are {', '.join(config_readers.keys())}."
)
)
raise ConfigurationError(emsg) from err


Expand Down Expand Up @@ -144,10 +148,10 @@ def reset_params(self) -> None:
self.update_params(**self._original_params)

def update_params(
self,
update_from_cfg_file: Optional[FilePath] = None,
**params: Any,
) -> None:
self,
update_from_cfg_file: Optional[FilePath] = None,
**params: Any,
) -> None:
"""
Update the modules parameters.
Expand Down Expand Up @@ -180,9 +184,8 @@ def update_params(
"""
if update_from_cfg_file and params:
_msg = (
"You can not provide both `update_from_cfg_file` "
"and key arguments."
)
"You can not provide both `update_from_cfg_file` " "and key arguments."
)
raise TypeError(_msg)

if update_from_cfg_file:
Expand All @@ -193,7 +196,7 @@ def update_params(
# the updating order is relevant
_n = recursive_dict_update(
non_mandatory_general_parameters_defaults, self._params
)
)
self._params = recursive_dict_update(_n, params)
self._fill_emptypaths()
self._confirm_fnames_exist()
Expand Down Expand Up @@ -226,8 +229,7 @@ def add_parent_to_paths(self) -> None:
return

@abstractmethod
def _run(self) -> None:
...
def _run(self) -> None: ...

def run(self, **params: Any) -> None:
"""Execute the module."""
Expand Down Expand Up @@ -294,9 +296,8 @@ def export_io_models(self, faulty_tolerance=0):
_msg = (
f"{faulty:.2f}% of output was not generated for this module "
f"and tolerance was set to {faulty_tolerance:.2f}%."
)
)
self.finish_with_error(_msg)


def finish_with_error(self, reason: object = "Module has failed.") -> None:
"""Finish with error message."""
Expand All @@ -307,9 +308,9 @@ def finish_with_error(self, reason: object = "Module has failed.") -> None:
raise RuntimeError(reason)

def _load_previous_io(
self,
filename: FilePath = MODULE_IO_FILE,
) -> ModuleIO:
self,
filename: FilePath = MODULE_IO_FILE,
) -> ModuleIO:
if self.order == 0:
self._num_of_input_molecules = 0
return ModuleIO()
Expand Down Expand Up @@ -337,10 +338,7 @@ def previous_path(self) -> Path:
@staticmethod
def last_step_folder(folders, index):
"""Retrieve last step folder."""
with_ind = [
folder for folder in folders
if int(folder.split('_')[0]) == index
]
with_ind = [folder for folder in folders if int(folder.split("_")[0]) == index]
nb_with_ind = len(with_ind)
# No matching index
if nb_with_ind == 0:
Expand All @@ -351,7 +349,7 @@ def last_step_folder(folders, index):
# Case of multiple matching index
else:
for folder in with_ind:
if folder.split('_')[-1] != INTERACTIVE_RE_SUFFIX:
if folder.split("_")[-1] != INTERACTIVE_RE_SUFFIX:
return folder
return with_ind[0]

Expand Down Expand Up @@ -389,9 +387,9 @@ def _fill_emptypaths(self) -> None:


def get_engine(
mode: str,
params: dict[Any, Any],
) -> partial[Union[HPCScheduler, Scheduler, MPIScheduler]]:
mode: str,
params: dict[Any, Any],
) -> partial[Union[HPCScheduler, Scheduler, MPIScheduler]]:
"""
Create an engine to run the jobs.
Expand All @@ -413,14 +411,14 @@ def get_engine(
target_queue=params["queue"],
queue_limit=params["queue_limit"],
concat=params["concat"],
)
)

elif mode == "local":
return partial( # type: ignore
Scheduler,
ncores=params["ncores"],
max_cpus=params["max_cpus"],
)
)
elif mode == "mpi":
return partial(MPIScheduler, ncores=params["ncores"]) # type: ignore

Expand All @@ -429,13 +427,13 @@ def get_engine(
raise ValueError(
f"Scheduler `mode` {mode!r} not recognized. "
f"Available options are {', '.join(available_engines)}"
)
)


def get_module_steps_folders(
folder: FilePath,
modules: Optional[Container[int]] = None,
) -> list[str]:
folder: FilePath,
modules: Optional[Container[int]] = None,
) -> list[str]:
"""
Return a sorted list of the step folders in a running directory.
Expand Down Expand Up @@ -468,16 +466,18 @@ def get_module_steps_folders(
steps = sorted(
(f for f in folders if step_folder_regex_re.search(f)),
key=lambda x: int(x.split("_")[0]),
)
)
if modules:
steps = [
st
for st in steps
if all([
int(st.split("_")[0]) in modules,
st.split("_")[1] in modules_names,
])
]
if all(
[
int(st.split("_")[0]) in modules,
st.split("_")[1] in modules_names,
]
)
]
return steps


Expand Down
2 changes: 2 additions & 0 deletions src/haddock/modules/defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,5 @@ less_io:
a network file system where I/O operations are slow.
group: "execution"
explevel: easy
incompatible:
mode: batch
Loading

0 comments on commit 7a9434e

Please sign in to comment.