Step-by-step custom constraints implementation
Users can customize various types of constraints, such as distance constraints, angle constraints, dihedral angle constraints, hydrogen bond constraints, etc
Some of the common scoring functions could be:
distance constraints
angle constraints
hydrogen bond constraints
The current framework implements distance constraints. In order to guide users to implement custom constraints, taking the example of adding angle constraints, in the following part,we will demonstrate in detail how to implement angle constraints
1. Define angle constraint class–AngleConstraintSF
The added custom constraints can be understood as a new scoring function, so first go to the scorer folder and open constraints.py
vim opendock/scorer/constraints.py
In the constrains.py file, it can be seen that distance constraints have been implemented, such as DistanceConstraintSF and DistanceMatrixConstraintSF. To add angle constraints, the first step is to implement an angle calculation function located in the base class ConstraintSF. The input is 3 coordinate points, and for ease of calculation, the output is the radian value of the angle.
def _angle(self, x, y, z):
# Calculate the two vectors
vec1 = x - y
vec2 = z - y
# Compute the dot product of the two vectors
dot_product = torch.sum(vec1 * vec2)
# Compute the magnitudes of the vectors
mag1 = torch.sqrt(torch.sum(torch.pow(vec1, 2)))
mag2 = torch.sqrt(torch.sum(torch.pow(vec2, 2)))
# Calculate the cosine of the angle using the dot product formula
cos_theta = dot_product / (mag1 * mag2)
# Ensure cos_theta is within the valid range [-1, 1] to avoid numerical errors
cos_theta = torch.clamp(cos_theta, -1.0, 1.0)
# Calculate the angle (in radians) using the arccosine function
angle = torch.acos(cos_theta)
return angle
Then we can define AngleConstraintSF, which inherits from the base class ConstraintSF
class AngleConstraintSF(ConstraintSF):
def __init__(self,
receptor = None,
ligand = None,
**kwargs):
super(AngleConstraintSF, self)\
.__init__(receptor=receptor, ligand=ligand)
self.grpA_mol_ = kwargs.pop('groupA_mol', "receptor")
self.grpB_mol_ = kwargs.pop('groupB_mol', "receptor") # receptor or ligand
self.grpC_mol_ = kwargs.pop('groupC_mol', "ligand")
self.grpA_idx_ = kwargs['grpA_ha_indices']
self.grpB_idx_ = kwargs['grpB_ha_indices']
self.grpC_idx_ = kwargs['grpC_ha_indices']
self.constraint_type_ = kwargs.pop('constraint', 'harmonic')
self.force_constant_ = kwargs.pop('force', 1.0)
#self.constraint_reference_ = kwargs.pop('reference', None)
# angle boundary, unit is angstrom
self.bounds_ = kwargs.pop('bounds', [0, np.pi])
assert (len(self.grpA_idx_) > 0 and len(self.grpB_idx_) > 0 and len(self.grpC_idx_) > 0)
def scoring(self):
if self.grpA_mol_.lower() in ['receptor', 'protein']:
_grpA_xyz = self.receptor.rec_heavy_atoms_xyz
elif self.grpA_mol_.lower() in ['ligand', 'molecule']:
_grpA_xyz = self.ligand.pose_heavy_atoms_coords[0]
if self.grpB_mol_.lower() in ['receptor', 'protein']:
_grpB_xyz = self.receptor.rec_heavy_atoms_xyz
elif self.grpB_mol_.lower() in ['ligand', 'molecule']:
_grpB_xyz = self.ligand.pose_heavy_atoms_coords[0]
if self.grpC_mol_.lower() in ['receptor', 'protein']:
_grpC_xyz = self.receptor.rec_heavy_atoms_xyz
elif self.grpC_mol_.lower() in ['ligand', 'molecule']:
_grpC_xyz = self.ligand.pose_heavy_atoms_coords[0]
pairs = list(zip(self.grpA_idx_, self.grpB_idx_,self.grpC_idx_))
self._angle_paired_ = []
for i, (atm1, atm2,atm3) in enumerate(pairs):
_a = self._angle(_grpA_xyz[atm1], _grpB_xyz[atm2], _grpC_xyz[atm3])
self._angle_paired_.append(_a)
self.angle_paired = torch.stack(self._angle_paired_)
score = self._apply_constraint(torch.mean(self.angle_paired))
return score.reshape((1, -1))
Thus, we successfully defined the angle constraint through the class AngleConstrainSF. Next, we will demonstrate how to use the newly defined angle constraint
2. Use newly defined angle constraints
Consistent with previous constraint cases, taking pdb: 3gzj as an example, we chose ligand atom CAF, protein side chain atoms ser87-OG, and GLY19-O as cases
Note
Please note that the residue index (residx) is generally 1-based as indicated in the PDB file.
The above atomic names have some differences between protein PDB files and PDBQT files, but the atoms are the same.
vim opendock/example/3gzj/atom_angle_constraint_example.py
indices_r1 = asl.select_atom(atomnames=['O', ], chains=['A'], residx=['10'], resnames=['GLY'])
print("indices_r1",indices_r1)
print(indices_r1, receptor.dataframe_ha_.head())
indices_r2 = asl.select_atom(atomnames=['OG', ], chains=['A'], residx=['78'], resnames=['SER'])
asl = AtomSelection(molecule=ligand)
indices_l = asl.select_atom(atomnames=['CAF', ])
cnstr = AngleConstraintSF(receptor, ligand,
grpA_ha_indices=indices_r1,
grpB_ha_indices=indices_r2,
grpC_ha_indices=indices_l,
constraint='wall',
bounds=[1.0698, 1.0698] #1.0698 indicate 61.3°, choose your custom angle upper and lower limits
)
print(cnstr.scoring())
# vina scoring function
sf1 = VinaSF(receptor, ligand)
vs = sf1.scoring()
print("Vina Score ", vs)
# combined scoring function
sf = HybridSF(receptor, ligand, scorers=[sf1, cnstr], weights=[0.5, 0.5])
vs = sf.scoring()
print("HybridSF Score ", vs)
So far, we have successfully defined the angle constraint and used it. Let’s develop your own custom constraints!