# Module for components shared between the protein-protein docking GUI
# and the backend
import json
from schrodinger.structure import Structure
from schrodinger.structutils import analyze
NUMBER_OF_ROTATIONS = 70000
MAX_NUMBER_OF_ROTATIONS = 70000
MULTIMER_ROTATIONS = 9994
EXAMPLE_CONSTRAINTS_FILE = """
[
{
"constraint_type" : "attraction",
"asl" : "res.num 2-3",
"protein_type" : "receptor",
"attraction" : 2.0
},
{
"constraint_type" : "repulsion",
"asl" : "res.num 1-2",
"protein_type" : "ligand"
},
{
"constraint_type" : "distance",
"required": 1,
"distance_pairs": [
{
"lig_asl" : "res.num 1",
"rec_asl" : "res.num 4",
"dmax" : 4.0,
"dmin" : 1.0
},
{
"lig_asl" : "res.num 3",
"rec_asl" : "res.num 1",
"dmax" : 5.0,
"dmin" : 1.5
}
]
}
]
"""
[docs]def get_residues_for_asl(st, asl):
"""
For a given input structure, st {schrodinger.Structure} and an
asl expressions that defines a residue subset, asl {string}, return
the list of residues that are in the asl expression. This
will be a list of tuples contraining the chain name {string} and
the combination of residue number and inscode {string} of each residue
Note: list is sorted to create more reproducible results
"""
residues = set()
for idx in analyze.evaluate_asl(st, asl):
atom = st.atom[idx]
# Note - I suspect this PIPER code will break for
# Note - I suspect this PIPER code will break for
# chain name = " " - DJG
if atom.inscode == " ":
residue = (atom.chain, "%s" % (atom.resnum))
else:
residue = (atom.chain, "%s%s" % (atom.resnum, atom.inscode))
residues.add(residue)
return sorted(list(residues))
[docs]class PIPERDistanceConstraintPair(object):
"""
Object used to store the pairs of residues and the allowable distaances
between them that make up a piper distance constraint.
"""
[docs] def __init__(self,
rec_asl: str = None,
lig_asl: str = None,
dmin: float = 0.0,
dmax: float = 5.0):
"""
Initialize with:
:param rec_asl: REQUIRED ASL espression corresponding to exactly one
residue on the receptor
:param lig_asl: REQUIRED ASL espression corresponding to exactly one
residue on the ligand
:param dmin: minimum allowed distance between any two atoms on the
residues defined by rec_asl and lig_asl
:param dmax: maximum allowed distance between any two atoms on the
residues defined by rec_asl and lig_asl
"""
self.rec_asl = rec_asl
self.lig_asl = lig_asl
self.dmin = dmin
self.dmax = dmax
if self.rec_asl is None or self.lig_asl is None:
raise RuntimeError("lig_asl and rec_asl must be defined for "
"PIPERDistanceConstraintPair")
def __str__(self):
return "Lig %s -- Rec %s Distance (%5.2f - %5.2f)" % (
self.lig_asl, self.rec_asl, self.dmin, self.dmax)
[docs] def toDict(self):
return {
'rec_asl': self.rec_asl,
'lig_asl': self.lig_asl,
'dmin': self.dmin,
'dmax': self.dmax
}
[docs] def setResidues(self, rec_st: Structure, lig_st: Structure):
"""
This must be called to set link the asl expresion given in the
initializer to a set of structures
:param rec_st: receptor structure
:param lig_st: receptor structure
If the ASL expression provided in the initializer does not correspond
to exactly one residue in rec_st and lig_st respectively than a
RuntimeError is raised
"""
self.rec_chain, self.rec_resi = self._setOneComponent(
"Receptor", rec_st, self.rec_asl)
self.lig_chain, self.lig_resi = self._setOneComponent(
"Ligand", lig_st, self.lig_asl)
def _setOneComponent(self, name, st, asl):
residues = get_residues_for_asl(st, asl)
if len(residues) != 1:
raise RuntimeError("%s ASL Expression must correspond to " % name +
"exactly 1 residue: %s -> %s " %
(asl, ",".join(residues)))
return residues[0][0], residues[0][1]
[docs] def getPiperDict(self):
"""
Return the dictionary that will be used to create the
piper-backend-formatted json file. This format is different than
the json format used to pass constraint objects to the
schrodinger-wrapper for Prime as that json file must include
distance and energy-based constraints.
"""
try:
return {
"rec_chain": self.rec_chain,
"rec_resi": self.rec_resi,
"lig_chain": self.lig_chain,
"lig_resi": self.lig_resi,
"dmin": self.dmin,
"dmax": self.dmax
}
except AttributeError as err:
raise AttributeError("Must call setResidues. (%s)" % err)
[docs]class PIPERConstraint(object):
""" Container for managing constraints """
ATTRACTION = 'attraction'
REPULSION = 'repulsion'
DISTANCE = 'distance'
RECEPTOR = 'receptor'
LIGAND = 'ligand'
ANTIBODY = 'antibody'
ANTIGEN = 'antigen'
[docs] def __init__(self,
constraint_type=None,
asl=None,
protein_type=None,
attraction=None,
required=None,
distance_pairs=None):
"""
"""
self.constraint_type = constraint_type
if self.isEnergy():
self.asl = asl
self.protein_type = protein_type
self.attraction = attraction
self.at_surface = (attraction == 0.0)
"""
Property holding the list of strings that can be added to a command
line list to identify residues in attraction/masking scripts.
"""
self.commandline_residues = []
elif self.isDistance():
self.distance_pairs = [
PIPERDistanceConstraintPair(**pair) for pair in distance_pairs
]
self.required = required
assert (type(self.required) == int)
else:
raise NotImplementedError("Incorrect constraint_type for "
"PIPERConstraint: %s" %
self.constraint_type)
[docs] def isEnergy(self):
""" Return True if this is an ENERGY-based constraint"""
return self.constraint_type in (self.ATTRACTION, self.REPULSION)
[docs] def isDistance(self):
""" Return True if this is an DISTANCE-based constraint"""
return self.constraint_type in (self.DISTANCE)
def __str__(self):
if self.isEnergy():
if self.attraction is None:
return "Constraint %s %s %s" % (self.protein_type,
self.constraint_type, self.asl)
else:
return "Constraint %s %s(%5.3f) %s" % (
self.protein_type, self.constraint_type, self.attraction,
self.asl)
elif self.isDistance():
return "Constraint %s (%d required) ( %s )" % (
self.constraint_type, self.required, ",".join(
["(%s)" % p for p in self.distance_pairs]))
[docs] def setResidues(self, rec_st, lig_st):
"""
Uses `self.asl` with `st_file` and to set the
commandline_residues property.
"""
if self.isEnergy():
# If protein_type is receptor OR we are running in multimer mode
# we need to use the receptor to later setResidues
if (self.protein_type
in (PIPERConstraint.RECEPTOR, PIPERConstraint.ANTIBODY)):
residues = get_residues_for_asl(rec_st, self.asl)
else:
residues = get_residues_for_asl(lig_st, self.asl)
self.commandline_residues = ["%s-%s" % (c, r) for c, r in residues]
elif self.isDistance():
for pair in self.distance_pairs:
pair.setResidues(rec_st, lig_st)
[docs] def getPiperDict(self):
"""
Return the dictionary that will be used to create the
piper-backend-formatted json file. This format is different than
the json format used to pass constraint objects to the
schrodinger-wrapper for Prime as that json file must include
This is used only for the distance constraints ( energy
constraints are passed to piper using command-line options) so
it will raise a NotImplementedError if used with an energy constraint
"""
if not self.isDistance():
raise NotImplementedError("Only Distance Constraints Need to "
"be written as Piper JSon Objects")
return {
'required': self.required,
'restraints': [pair.getPiperDict() for pair in self.distance_pairs]
}
[docs] @staticmethod
def read_constraints_file(fname):
"""
Reads a constraints file and returns the python representation
as loaded from the json module.
"""
with open(fname) as fh:
return json.load(fh)
[docs] @staticmethod
def write(constraints, ofile):
""" Writes a list of constraints to a JSON formatted file. """
json_obj = []
for constraint in constraints:
if constraint.isEnergy():
json_obj.append({
'asl': constraint.asl,
'constraint_type': constraint.constraint_type,
'protein_type': constraint.protein_type,
'attraction': constraint.attraction
})
elif constraint.isDistance():
json_obj.append({
'required': constraint.required,
'constraint_type': constraint.constraint_type,
'distance_pairs': [
pair.toDict() for pair in constraint.distance_pairs
]
})
with open(ofile, 'w') as fh:
return json.dump(json_obj, fh, indent=4)