Source code for bindflow.orchestration.flow_builder

import json
import os
import tarfile
from pathlib import Path
from typing import Union

import numpy as np

from bindflow import rules

PathLike = Union[os.PathLike, str, bytes]


[docs] def update_nwindows_config(config: dict) -> dict: """A simple function to update the config file for the entrance nwindows Parameters ---------- config : dict The configuration file with or without the nwindows keyword. In case it is present, must be in the shape of: .. code-block:: python 'nwindows': { 'ligand': { 'vdw': <int>[11], 'coul': <int>[11], }, 'complex': { 'vdw': <int>[21], 'coul': <int>[11], 'bonded': <int>[11] }, } Returns ------- dict The updated config """ nwindows_default = { 'ligand': { 'vdw': 11, 'coul': 11, }, 'complex': { 'vdw': 21, 'coul': 11, 'bonded': 11, }, } if 'nwindows' in config: nwindows = config['nwindows'] for key in ['ligand', 'complex']: if key in nwindows: nwindows_default[key].update(nwindows[key]) config['nwindows'] = nwindows_default return config
[docs] def generate_approach_snake_file(out_file_path: str, conf_file_path: str, calculation_type: str) -> None: """Used to generate the main Snakefile Parameters ---------- out_file_path : str Path to write the Snakefile conf_file_path : str Path of the yml workflow configuration file. calculation_type : str Either mmpbsa or fep. """ # Sanity check valid_calculation_type = ['mmpbsa', 'fep'] if calculation_type not in valid_calculation_type: raise ValueError(f"{calculation_type} is an invalid calculation_type, choose from {valid_calculation_type}") file_str = "# Load Config:\n"\ f"configfile: \'{conf_file_path}\'\n"\ "from pathlib import Path\n\n"\ "# Start Flow\n"\ f"include: \'{rules.super_flow}/Snakefile\'\n\n"\ "# Specify targets and dependencies\n"\ "rule RuleThemAll:\n" if calculation_type == 'fep': file_str += " input: Path(config[\"out_approach_path\"])/\"fep_results.csv\"" elif calculation_type == 'mmpbsa': file_str += " input: Path(config[\"out_approach_path\"])/\"mmxbsa_results.csv\"" with open(out_file_path, 'w') as out: out.write(file_str)
[docs] def approach_flow(global_config: dict, submit: bool = False) -> str: """It controls the rest of the workflows that make the actual calculations. It will only hang and wait till the rest subprocess finish. In case that cluster/options/job is defined in global_config, those options will be used to create the proper cluster submit script, if not cluster/option/calculation will be used instead Parameters ---------- global_config : dict The global configuration. It should contain: out_approach_path[PathLike], inputs[dict[dict]], water_model[str], host_name[str], host_selection[str] (no needed for mmpbsa), fix_protein[bool], solv_d[float], solv_bt[str], solv_rmin[float], solv_ion_conc[float] cofactor_on_protein[bool], cofactor_selection[str], extra_directives[dict], dt_max[float] ligand_names[list[str]], replicas[float], threads[int], samples[int] (no needed for fep) hmr_factor[float, None], custom_ff_path[str, None], cluster/type[str], cluster/options/calculation[dict] num_max_thread: int, The maximum number of threads to be used on each simulation. mdrun: dict: A dict of mdrun keywords to add to gmx mdrun, flag must be passed with boolean values. E.g {'cpi': True} extra_dependencies: A list of dependencies that must be run before gmx mdrun. Useful to launch modules as spack or conda. num_jobs: int: Maximum number of jobs to run in parallel cluster/options/job[dict]. The last is optional and will override cluster/options/calculation[dict] during submit submit : bool, optional Submit to the workload manager, by default False Returns ------- str Some identification of the submitted job. It will depend on how the submit method of the corresponded Schedular (:class:`bindflow.orchestration.generate_scheduler.Scheduler`) was implemented """ out_path = Path(global_config["out_approach_path"]) snake_path = out_path/"Snakefile" approach_conf_path = out_path/"snake_conf.json" approach_config = { "calculation_type": global_config["calculation_type"], "out_approach_path": str(global_config["out_approach_path"]), "inputs": global_config["inputs"], "water_model": global_config["water_model"], "host_name": global_config["host_name"], "fix_protein": global_config["fix_protein"], "solv_d": global_config["solv_d"], "solv_bt": global_config["solv_bt"], "solv_rmin": global_config["solv_rmin"], "solv_ion_conc": global_config["solv_ion_conc"], "cofactor_on_protein": global_config["cofactor_on_protein"], "cofactor_selection": global_config["cofactor_selection"], "ligand_names": global_config["ligand_names"], "replicas": global_config["replicas"], "hmr_factor": global_config["hmr_factor"], "custom_ff_path": global_config["custom_ff_path"], 'threads': global_config['threads'], 'extra_directives': global_config['extra_directives'], 'retries': 3, 'dt_max': global_config['dt_max'], # With this implementation the user can select the number of windows setting them up on the global configuration. } if global_config["calculation_type"] == 'fep': # Update number of windows if needed and create the lambda-schedule global_config = update_nwindows_config(global_config) approach_config['lambdas'] = { 'ligand': { 'vdw': list(np.round(np.linspace(0, 1, global_config['nwindows']['ligand']['vdw']), 2)), 'coul': list(np.round(np.linspace(0, 1, global_config['nwindows']['ligand']['coul']), 2)), }, 'complex': { 'vdw': list(np.round(np.linspace(0, 1, global_config['nwindows']['complex']['vdw']), 2)), 'coul': list(np.round(np.linspace(0, 1, global_config['nwindows']['complex']['coul']), 2)), 'bonded': list(np.round(np.linspace(0, 1, global_config['nwindows']['complex']['bonded']), 2)), }, } approach_config["host_selection"] = global_config["host_selection"] elif global_config["calculation_type"] == 'mmpbsa': approach_config["samples"] = global_config["samples"] if "mmpbsa" in global_config.keys(): approach_config["mmpbsa"] = global_config["mmpbsa"] # Specify the complex type if global_config["inputs"]["membrane"]: approach_config["complex_type"] = 'membrane' else: approach_config["complex_type"] = 'soluble' # Add extra mdp options if provided try: approach_config['mdp'] = global_config['mdp'] except KeyError: pass # Just to save the prefix if global_config["job_prefix"]: approach_config["job_prefix"] = global_config["job_prefix"] for ligand_definition in global_config["inputs"]["ligands"]: input_ligand_path = Path(ligand_definition['conf']) ligand_name = input_ligand_path.stem out_ligand_path = Path(global_config["out_approach_path"])/ligand_name # Make directories on demand out_ligand_path.mkdir(exist_ok=True, parents=True) out_ligand_input_path = out_ligand_path/"input" out_ligand_input_path.mkdir(exist_ok=True, parents=True) (out_ligand_input_path/"complex").mkdir(exist_ok=True, parents=True) (out_ligand_input_path/"ligand").mkdir(exist_ok=True, parents=True) # Archive original files with tarfile.open(out_ligand_input_path/'orig_in.tar.gz', "w:gz") as tar: tar.add(input_ligand_path, arcname=input_ligand_path.name) tar.add(global_config["inputs"]["protein"]["conf"], arcname=Path(global_config["inputs"]["protein"]["conf"]).name) if global_config["inputs"]["cofactor"]: tar.add(global_config["inputs"]["cofactor"]["conf"], arcname=Path(global_config["inputs"]["cofactor"]["conf"]).name) if global_config["inputs"]["membrane"]: tar.add(global_config["inputs"]["membrane"]["conf"], arcname=Path(global_config["inputs"]["membrane"]["conf"]).name) # Build the replicas for num_replica in range(1, global_config["replicas"] + 1): out_replica_path = out_ligand_path/str(num_replica) out_replica_path.mkdir(exist_ok=True, parents=True) with open(approach_conf_path, "w") as out_IO: json.dump(approach_config, out_IO, indent=4) generate_approach_snake_file(out_file_path=snake_path, conf_file_path=approach_conf_path, calculation_type=global_config["calculation_type"]) scheduler_class = global_config['scheduler_class'] scheduler = scheduler_class( # by default, run with the main cluster options # only if global_config["cluster"]["options"]["job"] is defined it will change during submit cluster_config=global_config["cluster"]["options"]["calculation"], out_dir=out_path, prefix_name=f"{global_config['job_prefix']}", snake_executor_file='job.sh') scheduler.build_snakemake(jobs=global_config["num_jobs"]) # Check for extra definitions if 'job' in global_config["cluster"]["options"]: job_cluster_config = global_config["cluster"]["options"]["job"] else: job_cluster_config = None # if global_config["cluster"]["options"]["job"] changes during submit the cluster options # Execute the pipeline in out_approach_path cwd = os.getcwd() os.chdir(global_config["out_approach_path"]) job_id = scheduler.submit(only_build=not submit, new_cluster_config=job_cluster_config, job_prefix=global_config["job_prefix"]) os.chdir(cwd) return job_id