import json
from pathlib import Path
from bindflow.utils.tools import List, PathLike, list_if_file
_MDP_PARAM_DEFAULT = {
"integrator": "steep",
"emtol": "1000.0",
"nsteps": "5000",
"nstlist": "10",
"cutoff-scheme": "Verlet",
"rlist": "1.0",
"vdwtype": "Cut-off",
"vdw-modifier": "Potential-shift-Verlet",
"rvdw-switch": "0",
"rvdw": "1.0",
"coulombtype": "pme",
"rcoulomb": "1.0",
"epsilon-r": "1",
"epsilon-rf": "1",
"constraints": "h-bonds",
"constraint-algorithm": "LINCS"
}
[docs]
class MDP:
"""Base class to work with MDP files
"""
[docs]
def __init__(self, **kwargs):
self.parameters = dict()
self._set_default_parameters()
self.set_parameters(**kwargs)
def _set_default_parameters(self):
# Add any default parameters here
self.parameters = _MDP_PARAM_DEFAULT
def set_parameters(self, **kwargs):
kwargs = {key.replace('_', '-'): value for key, value in kwargs.items()}
self.parameters.update(kwargs)
def from_file(self, template_filename, clean_current_parameters=True):
with open(template_filename, 'r') as f:
lines = f.readlines()
if clean_current_parameters:
# Clean all defined parameters
self.parameters = {}
for line in lines:
if line.startswith(';') or line.startswith('#'):
continue
tokens = line.strip().split('=', 1)
if len(tokens) != 2:
continue
parameter_name = tokens[0].strip().replace('_', '-')
parameter_value = tokens[1].strip()
self.parameters[parameter_name] = parameter_value
return self
def to_string(self):
s = ''
for parameter_name, parameter_value in self.parameters.items():
s += f'{parameter_name:<40} = {parameter_value}\n'
return s
def write(self, filename: str):
with open(filename, 'w') as f:
f.write(self.to_string())
def __repr__(self):
return f"{self.__class__.__name__}({json.dumps(self.parameters, indent=4)})"
[docs]
class StepMDP(MDP):
"""This subclass will inherit from :class:`bindflow.mdp.mdp.MDP`
It is meant to be used in combination with the templates that can
be access from ``bindflow.mdp.templates.TemplatePath``.
This class define the method ``set_new_step``. One time initialized,
the instance could be used to access other steps on the step_path
Parameters
----------
MDP : :class:`bindflow.mdp.mdp.MDP`
base MDP class
"""
[docs]
def __init__(self, step: str = None, step_path: PathLike = None, **kwargs):
"""Constructor. It is assume a tree directory as:
.. code-block:: text
.
├── emin.mdp
├── npt.mdp
├── npt-norest.mpd
├── nvt.mdp
└── prod.mdp
Parameters
----------
step : str, optional
the step, basically the name of the mdp file on templates, by default None
step_path : PathLike, optional
where to look for the mdp, by default None
Example
-------
.. ipython:: python
from bindflow.mdp import mdp
from bindflow.mdp.templates import TemplatePath
from pathlib import Path
my_mdp = mdp.StepMDP(step='00_min', step_path=Path(TemplatePath.ligand.fep)/'coul')
my_mdp.set_parameters(**{"init-lambda-state": "0", "coul-lambdas": "0 0.5 1"})
print(my_mdp)
my_mdp.set_new_step(step='01_nvt')
print(my_mdp.to_string())
"""
super().__init__(**kwargs)
self.step = step
self.step_path = Path(step_path)
if self.step:
self.__from_archive()
def set_new_step(self, step):
self.__from_archive(explicit_step=step)
return self
def __from_archive(self, explicit_step: str = None):
if explicit_step:
self.step = explicit_step
valid_steps = [step.stem for step in list_if_file(self.step_path, ext='.mdp')]
if self.step not in valid_steps:
raise ValueError(f"name = {self.step} is not a valid step mdp, must be one of: {valid_steps}")
self.from_file(self.step_path/f"{self.step}.mdp")
[docs]
def make_fep_dir_structure(sim_dir: PathLike, template_dir: PathLike, lambda_values: List[float], lambda_type: str, sys_type: str,
dt_max: float, mdp_extra_kwargs: dict = None):
"""This function is meant to be used on ``ligand_fep_setup`` and ``complex_fet_setup`` rules.
It will create the structure of the simulation directory: ``{sim_dir}/simulation/{lambda_type}.{i}/{step}/{step}.mdp``
Where:
* i: init-lambda-state,
* step: the name of the simulation to carry on
Parameters
----------
sim_dir : PathLike
Where the simulation suppose to run
template_dir : PathLike
This is the directory that storage the mdp templates: bindflow.mdp.templates.TemplatePath.ligand.fep or
bindlfow.mdp.templates.TemplatePath.complex.fep
lambda_values : List[float]
This is a the list of lambda values to be used inside the mdp on the entrance {lambda_type}-lambdas
lambda_type : str
Must be one of the following strings "vdw", "coul", "bonded" (the last is for restraints)
sys_type : str
Must one of the following strings "ligand" or "complex". This is used in order to turn on the bonded
lambdas for the complex simulations
mdp_extra_kwargs : dict
The MDP options for the fep calculations on every step. This dictionary must have the structure:
.. code-block:: text
{
'vdw':{
'step1': <mdp options>,
'step2': <mdp options>,
...
}
'coul':{
'step1': <mdp options>,
'step2': <mdp options>,
...
'bonded':{
'step1': <mdp options>,
'step2': <mdp options>,
...
}
}
Raises
------
ValueError
In case of an invalid ``lambda_type``
ValueError
In case of an invalid ``sys_type``
"""
sim_dir = Path(sim_dir)
template_dir = Path(template_dir)
valid_lambda_types = ["vdw", "coul", "bonded"]
valid_sys_types = ['ligand', 'complex']
if lambda_type not in valid_lambda_types:
raise ValueError(f"Non valid lambda_type = {lambda_type}. Must be one of {valid_lambda_types}")
if sys_type not in valid_sys_types:
raise ValueError(f"Non valid sys_type = {sys_type}. Must be one of {valid_sys_types}")
# Take from the source of the package what are the input MDP files
input_mdp = [step.name for step in list_if_file(template_dir/f"{lambda_type}", ext='.mdp')]
# Create the lambda string
lambda_range_str = " ".join(map(str, lambda_values))
# Create MDP template for fep calculations
mdp_template = StepMDP(step_path=template_dir/lambda_type)
for mdp_file in input_mdp:
step = Path(mdp_file).stem
# Update MDP step
mdp_template.set_new_step(step)
# Check dt and set dt_max if needed, this will be overwrite by the parameters provided in the mdp section of the config
if 'dt' in mdp_template.parameters: # Avoid min step, it assumes that the rest of the mdp templates steps have dt defined.
if float(mdp_template.parameters['dt'].split(';')[0]) > dt_max:
mdp_template.set_parameters(dt=dt_max)
# Set, if any, the user MDP options
if mdp_extra_kwargs:
try:
# TODO sanity check on the passed mdp options
mdp_template.set_parameters(**mdp_extra_kwargs[lambda_type][step])
except KeyError:
pass
# Update lambdas
mdp_template.set_parameters(**{f"{lambda_type}-lambdas": lambda_range_str})
# Set to 1 all the bonded-lambdas in case of vdw and coul for the complex
if sys_type.lower() == 'complex' and lambda_type in ['vdw', 'coul']:
mdp_template.set_parameters(**{"bonded-lambdas": " ".join(map(str, len(lambda_values)*[1]))})
for i in range(len(lambda_values)):
# Create simulation/state/step directory
(sim_dir/f"simulation/{lambda_type}.{i}/{step}").mkdir(exist_ok=True, parents=True)
# Update init-lambda-state
mdp_template.set_parameters(**{"init-lambda-state": i})
# Write MDP to the proper location
mdp_template.write(sim_dir/f"simulation/{lambda_type}.{i}/{step}/{step}.mdp")
def get_number_of_frames(input_mdp):
loaded_mdp_params = MDP().from_file(input_mdp).parameters
return -(int(loaded_mdp_params['nsteps'].split(';')[0]) // -int(loaded_mdp_params['nstxout-compressed'].split(';')[0])) # ceiling to the next int
if __name__ == "__main__":
pass