"""Boundary conditions for receptor based boundary conditions
including unbinding and rebinding.
"""
# Standard Library
import itertools as it
import logging
logger = logging.getLogger(__name__)
# Standard Library
import time
from collections import defaultdict
# Third Party Library
import numpy as np
from geomm.centering import center_around
from geomm.distance import minimum_distance
from geomm.grouping import group_pair
from geomm.rmsd import calc_rmsd
from geomm.superimpose import superimpose
# First Party Library
from wepy.boundary_conditions.boundary import WarpBC
from wepy.util.util import box_vectors_to_lengths_angles
from wepy.walker import WalkerState
[docs]
class ReceptorBC(WarpBC):
"""Abstract base class for ligand-receptor based boundary conditions.
Provides shared utilities for warping walkers to any number of
optionally weighted initial structures through a shared
`warp_walkers` method.
Non-abstract implementations of this class need only implement the
`_progress` method which should return a boolean signalling a
warping event and the dictionary-style warping record of the
progress for only a single walker. These records will be collated
into a single progress record across all walkers.
Additionally, the `_update_bc` method can be overriden to return
'BC' group records. That method should accept the arguments shown
in this ABC and return a list of dictionary-style 'BC' records.
Warping of walkers with multiple initial states will be done
according to a choice of initial states weighted on their weights,
if given.
"""
# records of boundary condition changes (sporadic)
BC_FIELDS = ()
BC_SHAPES = ()
BC_DTYPES = ()
BC_RECORD_FIELDS = ()
# warping fields are directly inherited
# progress towards the boundary conditions (continual)
PROGRESS_FIELDS = ()
PROGRESS_SHAPES = ()
PROGRESS_DTYPES = ()
PROGRESS_RECORD_FIELDS = ()
DISCONTINUITY_TARGET_IDXS = Ellipsis
"""Specifies which 'target_idxs' values are considered discontinuous targets.
Values are either integer indices, Ellipsis (indicating all
possible values are discontinuous), or None indicating no possible
value is discontinuous.
"""
def __init__(
self,
initial_states=None,
initial_weights=None,
ligand_idxs=None,
receptor_idxs=None,
**kwargs
):
"""Base constructor for ReceptorBC.
This should be called immediately in the subclass `__init__`
method.
If the initial weights for each initial state are not given
uniform weights are assigned to them.
Arguments
---------
ligand_idxs : arraylike of int
The indices of the atom positions in the state considered
the ligand.
receptor_idxs : arraylike of int
The indices of the atom positions in the state considered
the receptor.
Raises
------
AssertionError
If any of the following kwargs are not given:
ligand_idxs, receptor_idxs.
"""
super().__init__(
initial_states=initial_states, initial_weights=initial_weights, **kwargs
)
# make sure necessary inputs are given
assert ligand_idxs is not None, "Must give ligand indices"
assert receptor_idxs is not None, "Must give binding site indices"
self._ligand_idxs = ligand_idxs
self._receptor_idxs = receptor_idxs
@property
def ligand_idxs(self):
"""The indices of the atom positions in the state considered the ligand."""
return self._ligand_idxs
@property
def receptor_idxs(self):
"""The indices of the atom positions in the state considered the receptor."""
return self._receptor_idxs
[docs]
class RebindingBC(ReceptorBC):
"""Boundary condition for doing re-binding simulations of ligands to a
receptor.
Implements the ReceptorBC superclass.
This boundary condition will warp walkers to a number of initial
states whenever a walker becomes very close to the native (bound)
state.
Thus the choice of the 'initial_states' argument should be walkers
which are completely unbound (the choice of which are weighted by
'initial_weight') and the choice of 'native_state' should be of a
ligand bound to the receptor, e.g. X-ray crystallography or docked
structure.
The cutoff for the boundary is an RMSD of the walker to the native
state which is calculated by first aligning and superimposing the
entire structure according the atom indices specified in
'binding_site_idxs', and as the name suggests should correspond to
some approximation of the binding site of the ligand that occurs
in the native state. Then the raw RMSD of the native and walker
ligands is calculated. If this RMSD is less than the 'cutoff_rmsd'
argument the walker is warped.
PROGRESS is reported for each walker from this rmsd.
The BC records are never updated.
"""
# Records of boundary condition changes (sporadic)
BC_FIELDS = ReceptorBC.BC_FIELDS + ("native_rmsd_cutoff",)
"""The 'native_rmsd_cutoff' is the cutoff used to determine when
walkers have re-bound to the receptor, which is defined as the
RMSD of the ligand to the native ligand bound state, when the
binding sites are aligned and superimposed.
"""
BC_SHAPES = ReceptorBC.BC_SHAPES + ((1,),)
BC_DTYPES = ReceptorBC.BC_DTYPES + (float,)
BC_RECORD_FIELDS = ReceptorBC.BC_RECORD_FIELDS + ("native_rmsd_cutoff",)
# warping (sporadic)
WARPING_FIELDS = ReceptorBC.WARPING_FIELDS + ()
WARPING_SHAPES = ReceptorBC.WARPING_SHAPES + ()
WARPING_DTYPES = ReceptorBC.WARPING_DTYPES + ()
WARPING_RECORD_FIELDS = ReceptorBC.WARPING_RECORD_FIELDS + ()
# progress towards the boundary conditions (continual)
PROGRESS_FIELDS = ReceptorBC.PROGRESS_FIELDS + ("native_rmsd",)
PROGRESS_SHAPES = ReceptorBC.PROGRESS_SHAPES + (Ellipsis,)
PROGRESS_DTYPES = ReceptorBC.PROGRESS_DTYPES + (float,)
PROGRESS_RECORD_FIELDS = ReceptorBC.PROGRESS_RECORD_FIELDS + ("native_rmsd",)
"""Records for the state of this record group.
The 'native_rmsd' is the is the RMSD of the ligand to the native
ligand bound state, when the binding sites are aligned and
superimposed.
"""
def __init__(
self,
native_state=None,
cutoff_rmsd=0.2,
initial_states=None,
initial_weights=None,
ligand_idxs=None,
binding_site_idxs=None,
**kwargs
):
"""Constructor for RebindingBC.
Arguments
---------
native_state : object implementing the State interface
The reference bound state. Will be automatically centered.
cutoff_rmsd : float
The cutoff RMSD for considering a walker bound.
initial_states : list of objects implementing the State interface
The list of possible states that warped walkers will assume.
initial_weights : list of float, optional
List of normalized probabilities of the initial_states
provided. If not given, uniform probabilities will be
used.
ligand_idxs : arraylike of int
The indices of the atom positions in the state considered
the ligand.
binding_site_idxs : arraylike of int
The indices of the atom positions in the state considered
the binding site.
Raises
------
AssertionError
If any of the following kwargs are not given:
native_state, initial_states, ligand_idxs, receptor_idxs.
"""
super().__init__(
initial_states=initial_states,
initial_weights=initial_weights,
ligand_idxs=ligand_idxs,
receptor_idxs=binding_site_idxs**kwargs,
)
# test inputs
assert native_state is not None, "Must give a native state"
assert type(cutoff_rmsd) is float
native_state_d = native_state.dict()
# save the native state and center it around it's binding site
native_state_d["positions"] = center_around(
native_state["positions"], binding_site_idxs
)
native_state = WalkerState(**native_state_d)
# save attributes
self._native_state = native_state
self._cutoff_rmsd = cutoff_rmsd
@property
def native_state(self):
"""The reference bound state to which walkers are compared."""
return self._native_state
@property
def cutoff_rmsd(self):
"""The cutoff RMSD for considering a walker bound."""
return self._cutoff_rmsd
@property
def binding_site_idxs(self):
"""The indices of the atom positions in the state considered the binding site."""
return self._receptor_idxs
[docs]
def _progress(self, walker):
"""Calculate if the walker has bound and provide progress record.
Parameters
----------
walker : object implementing the Walker interface
Returns
-------
is_bound : bool
Whether the walker is unbound (warped) or not
progress_data : dict of str : value
Dictionary of the progress record group fields
for this walker alone.
"""
# first recenter the ligand and the receptor in the walker
box_lengths, box_angles = box_vectors_to_lengths_angles(
walker.state["box_vectors"]
)
grouped_walker_pos = group_pair(
walker.state["positions"],
box_lengths,
self.binding_site_idxs,
self.ligand_idxs,
)
# center the positions around the center of the binding site
centered_walker_pos = center_around(grouped_walker_pos, self.binding_site_idxs)
# superimpose the walker state positions over the native state
# matching the binding site indices only
sup_walker_pos, _, _ = superimpose(
self.native_state["positions"],
centered_walker_pos,
idxs=self.binding_site_idxs,
)
# calculate the rmsd of the walker ligand (superimposed
# according to the binding sites) to the native state ligand
native_rmsd = calc_rmsd(
self.native_state["positions"], sup_walker_pos, idxs=self.ligand_idxs
)
# test to see if the ligand is re-bound
rebound = False
if native_rmsd <= self.cutoff_rmsd:
rebound = True
progress_data = {"native_rmsd": native_rmsd}
return rebound, progress_data
[docs]
class UnbindingBC(ReceptorBC):
"""Boundary condition for ligand unbinding.
Walkers will be warped (discontinuously) if all atoms in the
ligand are at least a certain distance away from the atoms in the
receptor (i.e. the min-min of ligand-receptor distances > cutoff).
Warping will replace the walker state with the initial state given
as a parameter to this class.
Also reports on the progress of that min-min for each walker.
"""
# records of boundary condition changes (sporadic)
BC_FIELDS = ReceptorBC.BC_FIELDS + ("boundary_distance",)
"""
Only occurs at the start of the simulation and just reports on the
min-min cutoff distance.
"""
BC_SHAPES = ReceptorBC.BC_SHAPES + ((1,),)
BC_DTYPES = ReceptorBC.BC_DTYPES + (float,)
BC_RECORD_FIELDS = ReceptorBC.BC_RECORD_FIELDS + ("boundary_distance",)
# warping (sporadic)
WARPING_FIELDS = ReceptorBC.WARPING_FIELDS + ()
WARPING_SHAPES = ReceptorBC.WARPING_SHAPES + ()
WARPING_DTYPES = ReceptorBC.WARPING_DTYPES + ()
WARPING_RECORD_FIELDS = ReceptorBC.WARPING_RECORD_FIELDS + ()
# progress record group
PROGRESS_FIELDS = ReceptorBC.PROGRESS_FIELDS + ("min_distances",)
"""
The 'min_distances' field reports on the min-min ligand-receptor
distance for each walker.
"""
PROGRESS_SHAPES = ReceptorBC.PROGRESS_SHAPES + (Ellipsis,)
PROGRESS_DTYPES = ReceptorBC.PROGRESS_DTYPES + (float,)
PROGRESS_RECORD_FIELDS = ReceptorBC.PROGRESS_RECORD_FIELDS + ("min_distances",)
def __init__(
self,
initial_state=None,
cutoff_distance=1.0,
topology=None,
ligand_idxs=None,
receptor_idxs=None,
periodic=True,
**kwargs
):
"""Constructor for UnbindingBC class.
All the key-word arguments are necessary.
The 'initial_state' should be the initial state of your
simulation for proper non-equilibrium simulations.
Arguments
---------
initial_state : object implementing State interface
The state walkers will take on after unbinding.
cutoff_distance : float
The distance that specifies the boundary condition. When
the min-min ligand-receptor distance is less than this it
will be warped.
topology : str
A JSON string of topology. DEPRECATED: No longer needed
ligand_idxs : list of int
Indices of the atoms in the topology that correspond to the ligands.
receptor_idxs : list of int
Indices of the atoms in the topology that correspond to the
receptor for the ligand.
Raises
------
AssertionError
If any of the following are not provided: initial_state,
ligand_idxs, receptor_idxs
AssertionError
If the cutoff distance is not a float.
Warnings
--------
The 'initial_state' should be the initial state of your
simulation for proper non-equilibrium simulations.
"""
# since the super class can handle multiple initial states we
# wrap the single initial state to a list.
super().__init__(
initial_states=[initial_state],
ligand_idxs=ligand_idxs,
receptor_idxs=receptor_idxs,
**kwargs
)
# test input
assert type(cutoff_distance) is float
self._cutoff_distance = cutoff_distance
self._topology = topology
# whether or not to use the periodic box vectors in the
# distance calculation
self._periodic = periodic
@property
def cutoff_distance(self):
"""The distance a ligand must be to be unbound."""
return self._cutoff_distance
@property
def topology(self):
"""JSON string topology of the system.
Note: Deprecated and will be removed in future versions."""
return self._topology
[docs]
def _calc_min_distance(self, walker):
"""Min-min distance for a walker.
Parameters
----------
walker : object implementing the Walker interface
Returns
-------
min_distance : float
"""
# first recenter the ligand and the receptor in the walker
box_lengths, box_angles = box_vectors_to_lengths_angles(
walker.state["box_vectors"]
)
grouped_walker_pos = group_pair(
walker.state["positions"], box_lengths, self.receptor_idxs, self.ligand_idxs
)
min_dist = minimum_distance(
grouped_walker_pos[self.ligand_idxs], grouped_walker_pos[self.receptor_idxs]
)
return min_dist
[docs]
def _progress(self, walker):
"""Calculate whether a walker has unbound and also provide a
dictionary for a single walker in the progress records.
Parameters
----------
walker : object implementing the Walker interface
Returns
-------
is_unbound : bool
Whether the walker is unbound (warped) or not
progress_data : dict of str : value
Dictionary of the progress record group fields
for this walker alone.
"""
min_distance = self._calc_min_distance(walker)
# test to see if the ligand is unbound
unbound = False
if min_distance >= self._cutoff_distance:
unbound = True
progress_data = {"min_distances": min_distance}
return unbound, progress_data
[docs]
def _update_bc(self, new_walkers, warp_data, progress_data, cycle):
"""Perform an update to the boundary conditions.
This is only used on the first cycle to keep a record of the
cutoff parameter.
Parameters
----------
new_walkers : list of walkers
The walkers after warping.
warp_data : list of dict
progress_data : dict
cycle : int
Returns
-------
bc_data : list of dict
The dictionary-style records for BC update events
"""
# Only report a record on
# the first cycle which gives the distance at which walkers
# are warped
if cycle == 0:
return [
{
"boundary_distance": np.array([self._cutoff_distance]),
},
]
else:
return []