Merge pull request #50 from Electrostatics/nathan/import

Resolve cyclic import issues
This commit is contained in:
Nathan Baker
2020-06-03 17:50:30 -07:00
committed by GitHub
18 changed files with 1313 additions and 1296 deletions

View File

@@ -15,6 +15,6 @@ predictions." Journal of Chemical Theory and Computation 7, no. 2 (2011): 525-53
""" """
__all__ = ["atom", "bonds", "calculations", "conformation_container", __all__ = ["atom", "bonds", "calculations", "conformation_container",
"coupled_groups", "determinant", "determinants", "group", "coupled_groups", "determinant", "determinants", "group",
"hybrid36", "iterative", "lib", "ligand_pka_values", "ligand", "hybrid36", "iterative", "input", "lib", "ligand_pka_values",
"molecular_container", "output", "parameters", "pdb", "protonate", "ligand", "molecular_container", "output", "parameters",
"run", "vector_algebra", "version"] "protonate", "run", "vector_algebra", "version"]

View File

@@ -1,7 +1,6 @@
"""Atom class - contains all atom information found in the PDB file""" """Atom class - contains all atom information found in the PDB file"""
import string import string
import propka.lib from propka.lib import make_tidy_atom_label
import propka.group
from . import hybrid36 from . import hybrid36
@@ -26,7 +25,7 @@ STR_FMT = (
"({r.chain_id:1s}) [{r.x:>8.3f} {r.y:>8.3f} {r.z:>8.3f}] {r.element:s}") "({r.chain_id:1s}) [{r.x:>8.3f} {r.y:>8.3f} {r.z:>8.3f}] {r.element:s}")
class Atom(object): class Atom:
"""Atom class - contains all atom information found in the PDB file""" """Atom class - contains all atom information found in the PDB file"""
def __init__(self, line=None): def __init__(self, line=None):
@@ -50,6 +49,9 @@ class Atom(object):
self.z = None self.z = None
self.group = None self.group = None
self.group_type = None self.group_type = None
self.group_label = None
self.group_model_pka = None
self.group_model_pka_set = None
self.number_of_bonded_elements = {} self.number_of_bonded_elements = {}
self.cysteine_bridge = False self.cysteine_bridge = False
self.bonded_atoms = [] self.bonded_atoms = []
@@ -267,7 +269,7 @@ class Atom(object):
model_pka = PKA_FMT.format(self.group.model_pka) model_pka = PKA_FMT.format(self.group.model_pka)
str_ = INPUT_LINE_FMT.format( str_ = INPUT_LINE_FMT.format(
type=self.type.upper(), r=self, type=self.type.upper(), r=self,
atom_label=propka.lib.make_tidy_atom_label(self.name, self.element), atom_label=make_tidy_atom_label(self.name, self.element),
group=group, pka=model_pka) group=group, pka=model_pka)
return str_ return str_
@@ -313,21 +315,11 @@ class Atom(object):
self.occ = self.occ.replace('ALG', 'titratable_ligand') self.occ = self.occ.replace('ALG', 'titratable_ligand')
self.occ = self.occ.replace('BLG', 'titratable_ligand') self.occ = self.occ.replace('BLG', 'titratable_ligand')
self.occ = self.occ.replace('LG', 'non_titratable_ligand') self.occ = self.occ.replace('LG', 'non_titratable_ligand')
# try to initialise the group self.group_label = "{0:s}_group".format(self.occ)
try:
group_attr = "{0:s}_group".format(self.occ)
group_attr = getattr(propka.group, group_attr)
self.group = group_attr(self)
except:
# TODO - be more specific with expection handling here
str_ = (
'{0:s} in input_file is not recognized as a group'.format(
self.occ))
raise Exception(str_)
# set the model pKa value # set the model pKa value
if self.beta != '-': if self.beta != '-':
self.group.model_pka = float(self.beta) self.group_model_pka = float(self.beta)
self.group.model_pka_set = True self.group_model_pka_set = True
# set occ and beta to standard values # set occ and beta to standard values
self.occ = '1.00' self.occ = '1.00'
self.beta = '0.00' self.beta = '0.00'
@@ -344,7 +336,7 @@ class Atom(object):
""" """
str_ = PDB_LINE_FMT1.format( str_ = PDB_LINE_FMT1.format(
type=self.type.upper(), r=self, type=self.type.upper(), r=self,
atom_label=propka.lib.make_tidy_atom_label(self.name, self.element)) atom_label=make_tidy_atom_label(self.name, self.element))
return str_ return str_
def make_mol2_line(self, id_): def make_mol2_line(self, id_):
@@ -359,7 +351,7 @@ class Atom(object):
""" """
str_ = MOL2_LINE_FMT.format( str_ = MOL2_LINE_FMT.format(
id=id_, r=self, id=id_, r=self,
atom_label=propka.lib.make_tidy_atom_label(self.name, self.element)) atom_label=make_tidy_atom_label(self.name, self.element))
return str_ return str_
def make_pdb_line2(self, numb=None, name=None, res_name=None, chain_id=None, def make_pdb_line2(self, numb=None, name=None, res_name=None, chain_id=None,
@@ -397,7 +389,7 @@ class Atom(object):
str_ = PDB_LINE_FMT2.format( str_ = PDB_LINE_FMT2.format(
numb=numb, res_name=res_name, chain_id=chain_id, res_num=res_num, numb=numb, res_name=res_name, chain_id=chain_id, res_num=res_num,
x=x, y=y, z=z, occ=occ, beta=beta, x=x, y=y, z=z, occ=occ, beta=beta,
atom_label=propka.lib.make_tidy_atom_label(name, self.element) atom_label=make_tidy_atom_label(name, self.element)
) )
return str_ return str_
@@ -408,7 +400,7 @@ class Atom(object):
Returns: Returns:
String with label""" String with label"""
return propka.lib.make_tidy_atom_label(self.name, self.element) return make_tidy_atom_label(self.name, self.element)
def __str__(self): def __str__(self):
"""Return an undefined-format string version of this atom.""" """Return an undefined-format string version of this atom."""

View File

@@ -1,17 +1,5 @@
"""PROPKA calculations.""" """PROPKA calculations."""
import math import math
import propka.protonate
import propka.bonds
from propka.lib import warning, info
# TODO - this file should be broken into three separate files:
# * calculations.py - includes basic functions for calculating distances, etc.
# * hydrogen.py - includes bonding and protonation functions
# * energy.py - includes energy functions (dependent on distance functions)
# TODO - the next set of functions form a distinct "module" for distance calculation
# Maximum distance used to bound calculations of smallest distance # Maximum distance used to bound calculations of smallest distance
@@ -66,880 +54,3 @@ def get_smallest_distance(atoms1, atoms2):
res_atom1 = atom1 res_atom1 = atom1
res_atom2 = atom2 res_atom2 = atom2
return [res_atom1, math.sqrt(res_dist), res_atom2] return [res_atom1, math.sqrt(res_dist), res_atom2]
# TODO - the next set of functions form a distinct "module" for hydrogen addition
def setup_bonding_and_protonation(parameters, molecular_container):
"""Set up bonding and protonation for a molecule.
NOTE - the unused `parameters` argument is required for compatibility in version.py
TODO - figure out why there is a similar function in version.py
Args:
parameters: not used
molecular_container: molecule container.
"""
# make bonds
my_bond_maker = setup_bonding(molecular_container)
# set up ligand atom names
set_ligand_atom_names(molecular_container)
# apply information on pi electrons
my_bond_maker.add_pi_electron_information(molecular_container)
# Protonate atoms
if molecular_container.options.protonate_all:
PROTONATOR = propka.protonate.Protonate(verbose=False)
PROTONATOR.protonate(molecular_container)
def setup_bonding(molecular_container):
"""Set up bonding for a molecular container.
Args:
molecular_container: the molecular container in question
Returns:
BondMaker object
"""
my_bond_maker = propka.bonds.BondMaker()
my_bond_maker.find_bonds_for_molecules_using_boxes(molecular_container)
return my_bond_maker
def setup_bonding_and_protonation_30_style(parameters, molecular_container):
"""Set up bonding for a molecular container.
Args:
parameters: parameters for calculation
molecular_container: the molecular container in question
Returns:
BondMaker object
"""
# Protonate atoms
protonate_30_style(molecular_container)
# make bonds
bond_maker = propka.bonds.BondMaker()
bond_maker.find_bonds_for_molecules_using_boxes(molecular_container)
return bond_maker
def protonate_30_style(molecular_container):
"""Protonate the molecule.
Args:
molecular_container: molecule
"""
for name in molecular_container.conformation_names:
info('Now protonating', name)
# split atom into residues
curres = -1000000
residue = []
o_atom = None
c_atom = None
for atom in molecular_container.conformations[name].atoms:
if atom.res_num != curres:
curres = atom.res_num
if len(residue) > 0:
#backbone
[o_atom, c_atom] = add_backbone_hydrogen(
residue, o_atom, c_atom)
#arginine
if residue[0].res_name == 'ARG':
add_arg_hydrogen(residue)
#histidine
if residue[0].res_name == 'HIS':
add_his_hydrogen(residue)
#tryptophan
if residue[0].res_name == 'TRP':
add_trp_hydrogen(residue)
#amides
if residue[0].res_name in ['GLN', 'ASN']:
add_amd_hydrogen(residue)
residue = []
if atom.type == 'atom':
residue.append(atom)
def set_ligand_atom_names(molecular_container):
"""Set names for ligands in molecular container.
Args:
molecular_container: molecular container for ligand names
"""
for name in molecular_container.conformation_names:
molecular_container.conformations[name].set_ligand_atom_names()
def add_arg_hydrogen(residue):
"""Adds Arg hydrogen atoms to residues according to the 'old way'.
Args:
residue: arginine residue to protonate
Returns:
list of hydrogen atoms
"""
#info('Adding arg H',residue)
for atom in residue:
if atom.name == "CD":
cd_atom = atom
elif atom.name == "CZ":
cz_atom = atom
elif atom.name == "NE":
ne_atom = atom
elif atom.name == "NH1":
nh1_atom = atom
elif atom.name == "NH2":
nh2_atom = atom
h1_atom = protonate_sp2(cd_atom, ne_atom, cz_atom)
h1_atom.name = "HE"
h2_atom = protonate_direction(nh1_atom, ne_atom, cz_atom)
h2_atom.name = "HN1"
h3_atom = protonate_direction(nh1_atom, ne_atom, cd_atom)
h3_atom.name = "HN2"
h4_atom = protonate_direction(nh2_atom, ne_atom, cz_atom)
h4_atom.name = "HN3"
h5_atom = protonate_direction(nh2_atom, ne_atom, h1_atom)
h5_atom.name = "HN4"
return [h1_atom, h2_atom, h3_atom, h4_atom, h5_atom]
def add_his_hydrogen(residue):
"""Adds His hydrogen atoms to residues according to the 'old way'.
Args:
residue: histidine residue to protonate
"""
for atom in residue:
if atom.name == "CG":
cg_atom = atom
elif atom.name == "ND1":
nd_atom = atom
elif atom.name == "CD2":
cd_atom = atom
elif atom.name == "CE1":
ce_atom = atom
elif atom.name == "NE2":
ne_atom = atom
hd_atom = protonate_sp2(cg_atom, nd_atom, ce_atom)
hd_atom.name = "HND"
he_atom = protonate_sp2(cd_atom, ne_atom, ce_atom)
he_atom.name = "HNE"
def add_trp_hydrogen(residue):
"""Adds Trp hydrogen atoms to residues according to the 'old way'.
Args:
residue: tryptophan residue to protonate
"""
cd_atom = None
ne_atom = None
for atom in residue:
if atom.name == "CD1":
cd_atom = atom
elif atom.name == "NE1":
ne_atom = atom
elif atom.name == "CE2":
ce_atom = atom
if (cd_atom is None) or (ne_atom is None) or (ce_atom is None):
str_ = "Unable to find all atoms for {0:s} {1:s}".format(
residue[0].res_name, residue[0].res_num)
raise ValueError(str_)
he_atom = protonate_sp2(cd_atom, ne_atom, ce_atom)
he_atom.name = "HNE"
def add_amd_hydrogen(residue):
"""Adds Gln & Asn hydrogen atoms to residues according to the 'old way'.
Args:
residue: glutamine or asparagine residue to protonate
"""
c_atom = None
o_atom = None
n_atom = None
for atom in residue:
if ((atom.res_name == "GLN" and atom.name == "CD")
or (atom.res_name == "ASN" and atom.name == "CG")):
c_atom = atom
elif ((atom.res_name == "GLN" and atom.name == "OE1")
or (atom.res_name == "ASN" and atom.name == "OD1")):
o_atom = atom
elif ((atom.res_name == "GLN" and atom.name == "NE2")
or (atom.res_name == "ASN" and atom.name == "ND2")):
n_atom = atom
if (c_atom is None) or (o_atom is None) or (n_atom is None):
str_ = "Unable to find all atoms for {0:s} {1:s}".format(
residue[0].res_name, residue[0].res_num)
raise ValueError(str_)
h1_atom = protonate_direction(n_atom, o_atom, c_atom)
h1_atom.name = "HN1"
h2_atom = protonate_average_direction(n_atom, c_atom, o_atom)
h2_atom.name = "HN2"
def add_backbone_hydrogen(residue, o_atom, c_atom):
"""Adds hydrogen backbone atoms to residues according to the old way.
dR is wrong for the N-terminus (i.e. first residue) but it doesn't affect
anything at the moment. Could be improved, but works for now.
Args:
residue: residue to protonate
o_atom: backbone oxygen atom
c_atom: backbone carbon atom
Returns:
[new backbone oxygen atom, new backbone carbon atom]
"""
new_c_atom = None
new_o_atom = None
n_atom = None
for atom in residue:
if atom.name == "N":
n_atom = atom
if atom.name == "C":
new_c_atom = atom
if atom.name == "O":
new_o_atom = atom
if None in [c_atom, o_atom, n_atom]:
return [new_o_atom, new_c_atom]
if n_atom.res_name == "PRO":
# PRO doesn't have an H-atom; do nothing
pass
else:
h_atom = protonate_direction(n_atom, o_atom, c_atom)
h_atom.name = "H"
return [new_o_atom, new_c_atom]
def protonate_direction(x1_atom, x2_atom, x3_atom):
"""Protonates an atom, x1_atom, given a direction.
New direction for x1_atom proton is (x2_atom -> x3_atom).
Args:
x1_atom: atom to be protonated
x2_atom: atom for direction
x3_atom: other atom for direction
Returns:
new hydrogen atom
"""
dx = (x3_atom.x - x2_atom.x)
dy = (x3_atom.y - x2_atom.y)
dz = (x3_atom.z - x2_atom.z)
length = math.sqrt(dx*dx + dy*dy + dz*dz)
x = x1_atom.x + dx/length
y = x1_atom.y + dy/length
z = x1_atom.z + dz/length
h_atom = make_new_h(x1_atom, x, y, z)
h_atom.name = "H"
return h_atom
def protonate_average_direction(x1_atom, x2_atom, x3_atom):
"""Protonates an atom, x1_atom, given a direction.
New direction for x1_atom is (x1_atom/x2_atom -> x3_atom).
Note, this one uses the average of x1_atom & x2_atom (N & O) unlike
the previous N - C = O
Args:
x1_atom: atom to be protonated
x2_atom: atom for direction
x3_atom: other atom for direction
Returns:
new hydrogen atom
"""
dx = (x3_atom.x + x1_atom.x)*0.5 - x2_atom.x
dy = (x3_atom.y + x1_atom.y)*0.5 - x2_atom.y
dz = (x3_atom.z + x1_atom.z)*0.5 - x2_atom.z
length = math.sqrt(dx*dx + dy*dy + dz*dz)
x = x1_atom.x + dx/length
y = x1_atom.y + dy/length
z = x1_atom.z + dz/length
h_atom = make_new_h(x1_atom, x, y, z)
h_atom.name = "H"
return h_atom
def protonate_sp2(x1_atom, x2_atom, x3_atom):
"""Protonates a SP2 atom, given a list of atoms
Args:
x1_atom: atom to set direction
x2_atom: atom to be protonated
x3_atom: other atom to set direction
Returns:
new hydrogen atom
"""
dx = (x1_atom.x + x3_atom.x)*0.5 - x2_atom.x
dy = (x1_atom.y + x3_atom.y)*0.5 - x2_atom.y
dz = (x1_atom.z + x3_atom.z)*0.5 - x2_atom.z
length = math.sqrt(dx*dx + dy*dy + dz*dz)
x = x2_atom.x - dx/length
y = x2_atom.y - dy/length
z = x2_atom.z - dz/length
h_atom = make_new_h(x2_atom, x, y, z)
h_atom.name = "H"
return h_atom
def make_new_h(atom, x, y, z):
"""Add a new hydrogen to an atom at the specified position.
Args:
atom: atom to protonate
x: x position of hydrogen
y: y position of hydrogen
z: z position of hydrogen
Returns:
new hydrogen atom
"""
new_h = propka.atom.Atom()
new_h.set_property(
numb=None, name='H{0:s}'.format(atom.name[1:]),
res_name=atom.res_name, chain_id=atom.chain_id,
res_num=atom.res_num, x=x, y=y, z=z, occ=None, beta=None)
new_h.element = 'H'
new_h.bonded_atoms = [atom]
new_h.charge = 0
new_h.steric_number = 0
new_h.number_of_lone_pairs = 0
new_h.number_of_protons_to_add = 0
new_h.num_pi_elec_2_3_bonds = 0
atom.bonded_atoms.append(new_h)
atom.conformation_container.add_atom(new_h)
return new_h
# TODO - the remaining functions form a distinct "module" for desolvation
# TODO - I have no idea what these constants mean so I labeled them "UNK_"
UNK_MIN_DISTANCE = 2.75
MIN_DISTANCE_4TH = math.pow(UNK_MIN_DISTANCE, 4)
UNK_DIELECTRIC1 = 160
UNK_DIELECTRIC2 = 30
UNK_PKA_SCALING1 = 244.12
UNK_BACKBONE_DISTANCE1 = 6.0
UNK_BACKBONE_DISTANCE2 = 3.0
UNK_FANGLE_MIN = 0.001
UNK_PKA_SCALING2 = 0.8
COMBINED_NUM_BURIED_MAX = 900
SEPARATE_NUM_BURIED_MAX = 400
def radial_volume_desolvation(parameters, group):
"""Calculate desolvation terms for group.
Args:
parameters: parameters for desolvation calculation
group: group of atoms for calculation
"""
all_atoms = group.atom.conformation_container.get_non_hydrogen_atoms()
volume = 0.0
# TODO - Nathan really wants to rename the num_volume variable.
# He had to re-read the original paper to figure out what it was.
# A better name would be num_volume.
group.num_volume = 0
min_dist_4th = MIN_DISTANCE_4TH
for atom in all_atoms:
# ignore atoms in the same residue
if (atom.res_num == group.atom.res_num
and atom.chain_id == group.atom.chain_id):
continue
sq_dist = squared_distance(group, atom)
# desolvation
if sq_dist < parameters.desolv_cutoff_squared:
# use a default relative volume of 1.0 if the volume of the element
# is not found in parameters
# insert check for methyl groups
if atom.element == 'C' and atom.name not in ['CA', 'C']:
dvol = parameters.VanDerWaalsVolume['C4']
else:
dvol = parameters.VanDerWaalsVolume.get(atom.element, 1.0)
dv_inc = dvol/max(min_dist_4th, sq_dist*sq_dist)
volume += dv_inc
# buried
if sq_dist < parameters.buried_cutoff_squared:
group.num_volume += 1
group.buried = calculate_weight(parameters, group.num_volume)
scale_factor = calculate_scale_factor(parameters, group.buried)
volume_after_allowance = max(0.00, volume-parameters.desolvationAllowance)
group.energy_volume = (
group.charge * parameters.desolvationPrefactor
* volume_after_allowance * scale_factor)
def calculate_scale_factor(parameters, weight):
"""Calculate desolvation scaling factor.
Args:
parameters: parameters for desolvation calculation
weight: weight for scaling factor
Returns:
scaling factor
"""
scale_factor = 1.0 - (1.0 - parameters.desolvationSurfaceScalingFactor)*(1.0 - weight)
return scale_factor
def calculate_weight(parameters, num_volume):
"""Calculate the atom-based desolvation weight.
TODO - figure out why a similar function exists in version.py
Args:
parameters: parameters for desolvation calculation
num_volume: number of heavy atoms within desolvation calculation volume
Returns:
desolvation weight
"""
weight = (
float(num_volume - parameters.Nmin)
/ float(parameters.Nmax - parameters.Nmin))
weight = min(1.0, weight)
weight = max(0.0, weight)
return weight
def calculate_pair_weight(parameters, num_volume1, num_volume2):
"""Calculate the atom-pair based desolvation weight.
Args:
num_volume1: number of heavy atoms within first desolvation volume
num_volume2: number of heavy atoms within second desolvation volume
Returns:
desolvation weight
"""
num_volume = num_volume1 + num_volume2
num_min = 2*parameters.Nmin
num_max = 2*parameters.Nmax
weight = float(num_volume - num_min)/float(num_max - num_min)
weight = min(1.0, weight)
weight = max(0.0, weight)
return weight
def hydrogen_bond_energy(dist, dpka_max, cutoffs, f_angle=1.0):
"""Calculate hydrogen-bond interaction pKa shift.
Args:
dist: distance for hydrogen bond
dpka_max: maximum pKa value shift
cutoffs: array with max and min distance values
f_angle: angle scaling factor
Returns:
pKa shift value
"""
if dist < cutoffs[0]:
value = 1.00
elif dist > cutoffs[1]:
value = 0.00
else:
value = 1.0 - (dist - cutoffs[0])/(cutoffs[1] - cutoffs[0])
dpka = dpka_max*value*f_angle
return abs(dpka)
def angle_distance_factors(atom1=None, atom2=None, atom3=None, center=None):
"""Calculate distance and angle factors for three atoms for backbone interactions.
NOTE - you need to use atom1 to be the e.g. ASP atom if distance is reset at
return: [O1 -- H2-N3].
Also generalized to be able to be used for residue 'centers' for C=O COO interactions.
Args:
atom1: first atom for calculation (could be None)
atom2: second atom for calculation
atom3: third atom for calculation
center: center point between atoms 1 and 2
Returns
[distance factor between atoms 1 and 2,
angle factor,
distance factor between atoms 2 and 3]
"""
# The angle factor
#
# ---closest_atom1/2
# .
# .
# the_hydrogen---closest_atom2/1---
dx_32 = atom2.x - atom3.x
dy_32 = atom2.y - atom3.y
dz_32 = atom2.z - atom3.z
dist_23 = math.sqrt(dx_32 * dx_32 + dy_32 * dy_32 + dz_32 * dz_32)
dx_32 = dx_32/dist_23
dy_32 = dy_32/dist_23
dz_32 = dz_32/dist_23
if atom1 is None:
dx_21 = center[0] - atom2.x
dy_21 = center[1] - atom2.y
dz_21 = center[2] - atom2.z
else:
dx_21 = atom1.x - atom2.x
dy_21 = atom1.y - atom2.y
dz_21 = atom1.z - atom2.z
dist_12 = math.sqrt(dx_21 * dx_21 + dy_21 * dy_21 + dz_21 * dz_21)
dx_21 = dx_21/dist_12
dy_21 = dy_21/dist_12
dz_21 = dz_21/dist_12
f_angle = dx_21*dx_32 + dy_21*dy_32 + dz_21*dz_32
return dist_12, f_angle, dist_23
def hydrogen_bond_interaction(group1, group2, version):
"""Calculate energy for hydrogen bond interactions between two groups.
Args:
group1: first interacting group
group2: second interacting group
version: an object that contains version-specific parameters
Returns:
hydrogen bond interaction energy
"""
# find the smallest distance between interacting atoms
atoms1 = group1.get_interaction_atoms(group2)
atoms2 = group2.get_interaction_atoms(group1)
[closest_atom1, dist, closest_atom2] = get_smallest_distance(atoms1, atoms2)
if None in [closest_atom1, closest_atom2]:
warning(
'Side chain interaction failed for {0:s} and {1:s}'.format(
group1.label, group2.label))
return None
# get the parameters
[dpka_max, cutoff] = version.get_hydrogen_bond_parameters(closest_atom1,
closest_atom2)
if (dpka_max is None) or (None in cutoff):
return None
# check that the closest atoms are close enough
if dist >= cutoff[1]:
return None
# check that bond distance criteria is met
min_hbond_dist = version.parameters.min_bond_distance_for_hydrogen_bonds
if group1.atom.is_atom_within_bond_distance(group2.atom, min_hbond_dist, 1):
return None
# set angle factor
f_angle = 1.0
if group2.type in version.parameters.angular_dependent_sidechain_interactions:
if closest_atom2.element == 'H':
heavy_atom = closest_atom2.bonded_atoms[0]
hydrogen = closest_atom2
dist, f_angle, _ = angle_distance_factors(closest_atom1, hydrogen,
heavy_atom)
else:
# Either the structure is corrupt (no hydrogen), or the heavy atom
# is closer to the titratable atom than the hydrogen. In either
# case, we set the angle factor to 0
f_angle = 0.0
elif group1.type in version.parameters.angular_dependent_sidechain_interactions:
if closest_atom1.element == 'H':
heavy_atom = closest_atom1.bonded_atoms[0]
hydrogen = closest_atom1
dist, f_angle, _ = angle_distance_factors(closest_atom2, hydrogen,
heavy_atom)
else:
# Either the structure is corrupt (no hydrogen), or the heavy atom
# is closer to the titratable atom than the hydrogen. In either
# case, we set the angle factor to 0
f_angle = 0.0
weight = version.calculate_pair_weight(group1.num_volume, group2.num_volume)
exception, value = version.check_exceptions(group1, group2)
if exception:
# Do nothing, value should have been assigned.
pass
else:
value = version.calculate_side_chain_energy(
dist, dpka_max, cutoff, weight, f_angle)
return value
def electrostatic_interaction(group1, group2, dist, version):
"""Calculate electrostatic interaction betwee two groups.
Args:
group1: first interacting group
group2: second interacting group
dist: distance between groups
version: version-specific object with parameters and functions
Returns:
electrostatic interaction energy or None (if no interaction is appropriate)
"""
# check if we should do coulomb interaction at all
if not version.check_coulomb_pair(group1, group2, dist):
return None
weight = version.calculate_pair_weight(group1.num_volume, group2.num_volume)
value = version.calculate_coulomb_energy(dist, weight)
return value
def check_coulomb_pair(parameters, group1, group2, dist):
"""Checks if this Coulomb interaction should be done.
NOTE - this is a propka2.0 hack
TODO - figure out why a similar function exists in version.py
Args:
parameters: parameters for Coulomb calculations
group1: first interacting group
group2: second interacting group
dist: distance between groups
Returns:
Boolean
"""
num_volume = group1.num_volume + group2.num_volume
do_coulomb = True
# check if both groups are titratable (ions are taken care of in
# determinants::set_ion_determinants)
if not (group1.titratable and group2.titratable):
do_coulomb = False
# check if the distance is not too big
if dist > parameters.coulomb_cutoff2:
do_coulomb = False
# check that desolvation is ok
if num_volume < parameters.Nmin:
do_coulomb = False
return do_coulomb
def coulomb_energy(dist, weight, parameters):
"""Calculates the Coulomb interaction pKa shift based on Coulomb's law.
Args:
dist: distance for electrostatic interaction
weight: scaling of dielectric constant
parameters: parameter object for calculation
Returns:
pKa shift
"""
diel = UNK_DIELECTRIC1 - (UNK_DIELECTRIC1 - UNK_DIELECTRIC2)*weight
dist = max(dist, parameters.coulomb_cutoff1)
scale = (
(dist - parameters.coulomb_cutoff2)
/ (parameters.coulomb_cutoff1 - parameters.coulomb_cutoff2))
scale = max(0.0, scale)
scale = min(1.0, scale)
dpka = UNK_PKA_SCALING1/(diel*dist)*scale
return abs(dpka)
def backbone_reorganization(_, conformation):
"""Perform calculations related to backbone reorganizations.
NOTE - this was described in the code as "adding test stuff"
NOTE - this function does not appear to be used
TODO - figure out why a similar function exists in version.py
Args:
_: not used
conformation: specific molecule conformation
"""
titratable_groups = conformation.get_backbone_reorganisation_groups()
bbc_groups = conformation.get_backbone_co_groups()
for titratable_group in titratable_groups:
weight = titratable_group.buried
dpka = 0.00
for bbc_group in bbc_groups:
center = [titratable_group.x, titratable_group.y, titratable_group.z]
atom2 = bbc_group.get_interaction_atoms(titratable_group)[0]
dist, f_angle, _ = angle_distance_factors(atom2=atom2,
atom3=bbc_group.atom,
center=center)
if dist < UNK_BACKBONE_DISTANCE1 and f_angle > UNK_FANGLE_MIN:
value = (
1.0 - (dist-UNK_BACKBONE_DISTANCE2)
/ (UNK_BACKBONE_DISTANCE1-UNK_BACKBONE_DISTANCE2))
dpka += UNK_PKA_SCALING2 * min(1.0, value)
titratable_group.energy_local = dpka*weight
def check_exceptions(version, group1, group2):
"""Checks for atypical behavior in interactions between two groups.
Checks are made based on group type.
TODO - figure out why a similar function exists in version.py
Args:
version: version object
group1: first group for check
group2: second group for check
Returns:
1. Boolean indicating atypical behavior,
2. value associated with atypical interaction (None if Boolean is False)
"""
res_type1 = group1.type
res_type2 = group2.type
if (res_type1 == "COO") and (res_type2 == "ARG"):
exception, value = check_coo_arg_exception(group1, group2, version)
elif (res_type1 == "ARG") and (res_type2 == "COO"):
exception, value = check_coo_arg_exception(group2, group1, version)
elif (res_type1 == "COO") and (res_type2 == "COO"):
exception, value = check_coo_coo_exception(group1, group2, version)
elif (res_type1 == "CYS") and (res_type2 == "CYS"):
exception, value = check_cys_cys_exception(group1, group2, version)
elif ((res_type1 == "COO") and (res_type2 == "HIS")
or (res_type1 == "HIS") and (res_type2 == "COO")):
exception, value = check_coo_his_exception(group1, group2, version)
elif ((res_type1 == "OCO") and (res_type2 == "HIS")
or (res_type1 == "HIS") and (res_type2 == "OCO")):
exception, value = check_oco_his_exception(group1, group2, version)
elif ((res_type1 == "CYS") and (res_type2 == "HIS")
or (res_type1 == "HIS") and (res_type2 == "CYS")):
exception, value = check_cys_his_exception(group1, group2, version)
else:
# do nothing, no exception for this pair
exception = False
value = None
return exception, value
def check_coo_arg_exception(group_coo, group_arg, version):
"""Check for COO-ARG interaction atypical behavior.
Uses the two shortest unique distances (involving 2+2 atoms)
Args:
group_coo: COO group
group_arg: ARG group
version: version object
Returns:
1. Boolean indicating atypical behavior,
2. value associated with atypical interaction (None if Boolean is False)
"""
exception = True
value_tot = 0.00
# needs to be this way since you want to find shortest distance first
atoms_coo = []
atoms_coo.extend(group_coo.get_interaction_atoms(group_arg))
atoms_arg = []
atoms_arg.extend(group_arg.get_interaction_atoms(group_coo))
for _ in ["shortest", "runner-up"]:
# find the closest interaction pair
[closest_coo_atom, dist, closest_arg_atom] = get_smallest_distance(atoms_coo,
atoms_arg)
[dpka_max, cutoff] = version.get_hydrogen_bond_parameters(closest_coo_atom,
closest_arg_atom)
# calculate and sum up interaction energy
f_angle = 1.00
if group_arg.type in version.parameters.angular_dependent_sidechain_interactions:
atom3 = closest_arg_atom.bonded_atoms[0]
dist, f_angle, _ = angle_distance_factors(closest_coo_atom,
closest_arg_atom,
atom3)
value = hydrogen_bond_energy(dist, dpka_max, cutoff, f_angle)
value_tot += value
# remove closest atoms before we attemp to find the runner-up pair
atoms_coo.remove(closest_coo_atom)
atoms_arg.remove(closest_arg_atom)
return exception, value_tot
def check_coo_coo_exception(group1, group2, version):
"""Check for COO-COO hydrogen-bond atypical interaction behavior.
Args:
group1: first group for check
group2: second group for check
version: version object
Returns:
1. Boolean indicating atypical behavior,
2. value associated with atypical interaction (None if Boolean is False)
"""
exception = True
interact_groups12 = group1.get_interaction_atoms(group2)
interact_groups21 = group2.get_interaction_atoms(group1)
[closest_atom1, dist, closest_atom2] = get_smallest_distance(interact_groups12,
interact_groups21)
[dpka_max, cutoff] = version.get_hydrogen_bond_parameters(closest_atom1,
closest_atom2)
f_angle = 1.00
value = hydrogen_bond_energy(dist, dpka_max, cutoff, f_angle)
weight = calculate_pair_weight(version.parameters, group1.num_volume, group2.num_volume)
value = value * (1.0 + weight)
return exception, value
def check_coo_his_exception(group1, group2, version):
"""Check for COO-HIS atypical interaction behavior
Args:
group1: first group for check
group2: second group for check
version: version object
Returns:
1. Boolean indicating atypical behavior,
2. value associated with atypical interaction (None if Boolean is False)
"""
exception = False
if check_buried(group1.num_volume, group2.num_volume):
exception = True
return exception, version.parameters.COO_HIS_exception
def check_oco_his_exception(group1, group2, version):
"""Check for OCO-HIS atypical interaction behavior
Args:
group1: first group for check
group2: second group for check
version: version object
Returns:
1. Boolean indicating atypical behavior,
2. value associated with atypical interaction (None if Boolean is False)
"""
exception = False
if check_buried(group1.num_volume, group2.num_volume):
exception = True
return exception, version.parameters.OCO_HIS_exception
def check_cys_his_exception(group1, group2, version):
"""Check for CYS-HIS atypical interaction behavior
Args:
group1: first group for check
group2: second group for check
version: version object
Returns:
1. Boolean indicating atypical behavior,
2. value associated with atypical interaction (None if Boolean is False)
"""
exception = False
if check_buried(group1.num_volume, group2.num_volume):
exception = True
return exception, version.parameters.CYS_HIS_exception
def check_cys_cys_exception(group1, group2, version):
"""Check for CYS-CYS atypical interaction behavior
Args:
group1: first group for check
group2: second group for check
version: version object
Returns:
1. Boolean indicating atypical behavior,
2. value associated with atypical interaction (None if Boolean is False)
"""
exception = False
if check_buried(group1.num_volume, group2.num_volume):
exception = True
return exception, version.parameters.CYS_CYS_exception
def check_buried(num_volume1, num_volume2):
"""Check to see if an interaction is buried
Args:
num_volume1: number of buried heavy atoms in volume 1
num_volume2: number of buried heavy atoms in volume 2
Returns:
True if interaction is buried, False otherwise
"""
if ((num_volume1 + num_volume2 <= COMBINED_NUM_BURIED_MAX)
and (num_volume1 <= SEPARATE_NUM_BURIED_MAX
or num_volume2 <= SEPARATE_NUM_BURIED_MAX)):
return False
return True

View File

@@ -36,7 +36,6 @@ class ConformationContainer:
self.groups = [] self.groups = []
self.chains = [] self.chains = []
self.current_iter_item = 0 self.current_iter_item = 0
# TODO - what is marvin_pkas_calculated?
self.marvin_pkas_calculated = False self.marvin_pkas_calculated = False
self.non_covalently_coupled_groups = False self.non_covalently_coupled_groups = False

View File

@@ -8,7 +8,7 @@ import propka.iterative
import propka.lib import propka.lib
import propka.vector_algebra import propka.vector_algebra
from propka.calculations import squared_distance, get_smallest_distance from propka.calculations import squared_distance, get_smallest_distance
from propka.calculations import angle_distance_factors, hydrogen_bond_energy from propka.energy import angle_distance_factors, hydrogen_bond_energy
from propka.determinant import Determinant from propka.determinant import Determinant

531
propka/energy.py Normal file
View File

@@ -0,0 +1,531 @@
"""Energy calculations."""
import math
from propka.lib import warning
from propka.calculations import squared_distance, get_smallest_distance
# TODO - I have no idea what these constants mean so I labeled them "UNK_"
UNK_MIN_DISTANCE = 2.75
MIN_DISTANCE_4TH = math.pow(UNK_MIN_DISTANCE, 4)
UNK_DIELECTRIC1 = 160
UNK_DIELECTRIC2 = 30
UNK_PKA_SCALING1 = 244.12
UNK_BACKBONE_DISTANCE1 = 6.0
UNK_BACKBONE_DISTANCE2 = 3.0
UNK_FANGLE_MIN = 0.001
UNK_PKA_SCALING2 = 0.8
COMBINED_NUM_BURIED_MAX = 900
SEPARATE_NUM_BURIED_MAX = 400
def radial_volume_desolvation(parameters, group):
"""Calculate desolvation terms for group.
Args:
parameters: parameters for desolvation calculation
group: group of atoms for calculation
"""
all_atoms = group.atom.conformation_container.get_non_hydrogen_atoms()
volume = 0.0
group.num_volume = 0
min_dist_4th = MIN_DISTANCE_4TH
for atom in all_atoms:
# ignore atoms in the same residue
if (atom.res_num == group.atom.res_num
and atom.chain_id == group.atom.chain_id):
continue
sq_dist = squared_distance(group, atom)
# desolvation
if sq_dist < parameters.desolv_cutoff_squared:
# use a default relative volume of 1.0 if the volume of the element
# is not found in parameters
# insert check for methyl groups
if atom.element == 'C' and atom.name not in ['CA', 'C']:
dvol = parameters.VanDerWaalsVolume['C4']
else:
dvol = parameters.VanDerWaalsVolume.get(atom.element, 1.0)
dv_inc = dvol/max(min_dist_4th, sq_dist*sq_dist)
volume += dv_inc
# buried
if sq_dist < parameters.buried_cutoff_squared:
group.num_volume += 1
group.buried = calculate_weight(parameters, group.num_volume)
scale_factor = calculate_scale_factor(parameters, group.buried)
volume_after_allowance = max(0.00, volume-parameters.desolvationAllowance)
group.energy_volume = (
group.charge * parameters.desolvationPrefactor
* volume_after_allowance * scale_factor)
def calculate_scale_factor(parameters, weight):
"""Calculate desolvation scaling factor.
Args:
parameters: parameters for desolvation calculation
weight: weight for scaling factor
Returns:
scaling factor
"""
scale_factor = 1.0 - (1.0 - parameters.desolvationSurfaceScalingFactor)*(1.0 - weight)
return scale_factor
def calculate_weight(parameters, num_volume):
"""Calculate the atom-based desolvation weight.
TODO - figure out why a similar function exists in version.py
Args:
parameters: parameters for desolvation calculation
num_volume: number of heavy atoms within desolvation calculation volume
Returns:
desolvation weight
"""
weight = (
float(num_volume - parameters.Nmin)
/ float(parameters.Nmax - parameters.Nmin))
weight = min(1.0, weight)
weight = max(0.0, weight)
return weight
def calculate_pair_weight(parameters, num_volume1, num_volume2):
"""Calculate the atom-pair based desolvation weight.
Args:
num_volume1: number of heavy atoms within first desolvation volume
num_volume2: number of heavy atoms within second desolvation volume
Returns:
desolvation weight
"""
num_volume = num_volume1 + num_volume2
num_min = 2*parameters.Nmin
num_max = 2*parameters.Nmax
weight = float(num_volume - num_min)/float(num_max - num_min)
weight = min(1.0, weight)
weight = max(0.0, weight)
return weight
def hydrogen_bond_energy(dist, dpka_max, cutoffs, f_angle=1.0):
"""Calculate hydrogen-bond interaction pKa shift.
Args:
dist: distance for hydrogen bond
dpka_max: maximum pKa value shift
cutoffs: array with max and min distance values
f_angle: angle scaling factor
Returns:
pKa shift value
"""
if dist < cutoffs[0]:
value = 1.00
elif dist > cutoffs[1]:
value = 0.00
else:
value = 1.0 - (dist - cutoffs[0])/(cutoffs[1] - cutoffs[0])
dpka = dpka_max*value*f_angle
return abs(dpka)
def angle_distance_factors(atom1=None, atom2=None, atom3=None, center=None):
"""Calculate distance and angle factors for three atoms for backbone interactions.
NOTE - you need to use atom1 to be the e.g. ASP atom if distance is reset at
return: [O1 -- H2-N3].
Also generalized to be able to be used for residue 'centers' for C=O COO interactions.
Args:
atom1: first atom for calculation (could be None)
atom2: second atom for calculation
atom3: third atom for calculation
center: center point between atoms 1 and 2
Returns
[distance factor between atoms 1 and 2,
angle factor,
distance factor between atoms 2 and 3]
"""
# The angle factor
#
# ---closest_atom1/2
# .
# .
# the_hydrogen---closest_atom2/1---
dx_32 = atom2.x - atom3.x
dy_32 = atom2.y - atom3.y
dz_32 = atom2.z - atom3.z
dist_23 = math.sqrt(dx_32 * dx_32 + dy_32 * dy_32 + dz_32 * dz_32)
dx_32 = dx_32/dist_23
dy_32 = dy_32/dist_23
dz_32 = dz_32/dist_23
if atom1 is None:
dx_21 = center[0] - atom2.x
dy_21 = center[1] - atom2.y
dz_21 = center[2] - atom2.z
else:
dx_21 = atom1.x - atom2.x
dy_21 = atom1.y - atom2.y
dz_21 = atom1.z - atom2.z
dist_12 = math.sqrt(dx_21 * dx_21 + dy_21 * dy_21 + dz_21 * dz_21)
dx_21 = dx_21/dist_12
dy_21 = dy_21/dist_12
dz_21 = dz_21/dist_12
f_angle = dx_21*dx_32 + dy_21*dy_32 + dz_21*dz_32
return dist_12, f_angle, dist_23
def hydrogen_bond_interaction(group1, group2, version):
"""Calculate energy for hydrogen bond interactions between two groups.
Args:
group1: first interacting group
group2: second interacting group
version: an object that contains version-specific parameters
Returns:
hydrogen bond interaction energy
"""
# find the smallest distance between interacting atoms
atoms1 = group1.get_interaction_atoms(group2)
atoms2 = group2.get_interaction_atoms(group1)
[closest_atom1, dist, closest_atom2] = get_smallest_distance(atoms1, atoms2)
if None in [closest_atom1, closest_atom2]:
warning(
'Side chain interaction failed for {0:s} and {1:s}'.format(
group1.label, group2.label))
return None
# get the parameters
[dpka_max, cutoff] = version.get_hydrogen_bond_parameters(closest_atom1,
closest_atom2)
if (dpka_max is None) or (None in cutoff):
return None
# check that the closest atoms are close enough
if dist >= cutoff[1]:
return None
# check that bond distance criteria is met
min_hbond_dist = version.parameters.min_bond_distance_for_hydrogen_bonds
if group1.atom.is_atom_within_bond_distance(group2.atom, min_hbond_dist, 1):
return None
# set angle factor
f_angle = 1.0
if group2.type in version.parameters.angular_dependent_sidechain_interactions:
if closest_atom2.element == 'H':
heavy_atom = closest_atom2.bonded_atoms[0]
hydrogen = closest_atom2
dist, f_angle, _ = angle_distance_factors(closest_atom1, hydrogen,
heavy_atom)
else:
# Either the structure is corrupt (no hydrogen), or the heavy atom
# is closer to the titratable atom than the hydrogen. In either
# case, we set the angle factor to 0
f_angle = 0.0
elif group1.type in version.parameters.angular_dependent_sidechain_interactions:
if closest_atom1.element == 'H':
heavy_atom = closest_atom1.bonded_atoms[0]
hydrogen = closest_atom1
dist, f_angle, _ = angle_distance_factors(closest_atom2, hydrogen,
heavy_atom)
else:
# Either the structure is corrupt (no hydrogen), or the heavy atom
# is closer to the titratable atom than the hydrogen. In either
# case, we set the angle factor to 0
f_angle = 0.0
weight = version.calculate_pair_weight(group1.num_volume, group2.num_volume)
exception, value = version.check_exceptions(group1, group2)
if exception:
# Do nothing, value should have been assigned.
pass
else:
value = version.calculate_side_chain_energy(
dist, dpka_max, cutoff, weight, f_angle)
return value
def electrostatic_interaction(group1, group2, dist, version):
"""Calculate electrostatic interaction betwee two groups.
Args:
group1: first interacting group
group2: second interacting group
dist: distance between groups
version: version-specific object with parameters and functions
Returns:
electrostatic interaction energy or None (if no interaction is appropriate)
"""
# check if we should do coulomb interaction at all
if not version.check_coulomb_pair(group1, group2, dist):
return None
weight = version.calculate_pair_weight(group1.num_volume, group2.num_volume)
value = version.calculate_coulomb_energy(dist, weight)
return value
def check_coulomb_pair(parameters, group1, group2, dist):
"""Checks if this Coulomb interaction should be done.
NOTE - this is a propka2.0 hack
TODO - figure out why a similar function exists in version.py
Args:
parameters: parameters for Coulomb calculations
group1: first interacting group
group2: second interacting group
dist: distance between groups
Returns:
Boolean
"""
num_volume = group1.num_volume + group2.num_volume
do_coulomb = True
# check if both groups are titratable (ions are taken care of in
# determinants::set_ion_determinants)
if not (group1.titratable and group2.titratable):
do_coulomb = False
# check if the distance is not too big
if dist > parameters.coulomb_cutoff2:
do_coulomb = False
# check that desolvation is ok
if num_volume < parameters.Nmin:
do_coulomb = False
return do_coulomb
def coulomb_energy(dist, weight, parameters):
"""Calculates the Coulomb interaction pKa shift based on Coulomb's law.
Args:
dist: distance for electrostatic interaction
weight: scaling of dielectric constant
parameters: parameter object for calculation
Returns:
pKa shift
"""
diel = UNK_DIELECTRIC1 - (UNK_DIELECTRIC1 - UNK_DIELECTRIC2)*weight
dist = max(dist, parameters.coulomb_cutoff1)
scale = (
(dist - parameters.coulomb_cutoff2)
/ (parameters.coulomb_cutoff1 - parameters.coulomb_cutoff2))
scale = max(0.0, scale)
scale = min(1.0, scale)
dpka = UNK_PKA_SCALING1/(diel*dist)*scale
return abs(dpka)
def backbone_reorganization(_, conformation):
"""Perform calculations related to backbone reorganizations.
NOTE - this was described in the code as "adding test stuff"
NOTE - this function does not appear to be used
TODO - figure out why a similar function exists in version.py
Args:
_: not used
conformation: specific molecule conformation
"""
titratable_groups = conformation.get_backbone_reorganisation_groups()
bbc_groups = conformation.get_backbone_co_groups()
for titratable_group in titratable_groups:
weight = titratable_group.buried
dpka = 0.00
for bbc_group in bbc_groups:
center = [titratable_group.x, titratable_group.y, titratable_group.z]
atom2 = bbc_group.get_interaction_atoms(titratable_group)[0]
dist, f_angle, _ = angle_distance_factors(atom2=atom2,
atom3=bbc_group.atom,
center=center)
if dist < UNK_BACKBONE_DISTANCE1 and f_angle > UNK_FANGLE_MIN:
value = (
1.0 - (dist-UNK_BACKBONE_DISTANCE2)
/ (UNK_BACKBONE_DISTANCE1-UNK_BACKBONE_DISTANCE2))
dpka += UNK_PKA_SCALING2 * min(1.0, value)
titratable_group.energy_local = dpka*weight
def check_exceptions(version, group1, group2):
"""Checks for atypical behavior in interactions between two groups.
Checks are made based on group type.
TODO - figure out why a similar function exists in version.py
Args:
version: version object
group1: first group for check
group2: second group for check
Returns:
1. Boolean indicating atypical behavior,
2. value associated with atypical interaction (None if Boolean is False)
"""
res_type1 = group1.type
res_type2 = group2.type
if (res_type1 == "COO") and (res_type2 == "ARG"):
exception, value = check_coo_arg_exception(group1, group2, version)
elif (res_type1 == "ARG") and (res_type2 == "COO"):
exception, value = check_coo_arg_exception(group2, group1, version)
elif (res_type1 == "COO") and (res_type2 == "COO"):
exception, value = check_coo_coo_exception(group1, group2, version)
elif (res_type1 == "CYS") and (res_type2 == "CYS"):
exception, value = check_cys_cys_exception(group1, group2, version)
elif ((res_type1 == "COO") and (res_type2 == "HIS")
or (res_type1 == "HIS") and (res_type2 == "COO")):
exception, value = check_coo_his_exception(group1, group2, version)
elif ((res_type1 == "OCO") and (res_type2 == "HIS")
or (res_type1 == "HIS") and (res_type2 == "OCO")):
exception, value = check_oco_his_exception(group1, group2, version)
elif ((res_type1 == "CYS") and (res_type2 == "HIS")
or (res_type1 == "HIS") and (res_type2 == "CYS")):
exception, value = check_cys_his_exception(group1, group2, version)
else:
# do nothing, no exception for this pair
exception = False
value = None
return exception, value
def check_coo_arg_exception(group_coo, group_arg, version):
"""Check for COO-ARG interaction atypical behavior.
Uses the two shortest unique distances (involving 2+2 atoms)
Args:
group_coo: COO group
group_arg: ARG group
version: version object
Returns:
1. Boolean indicating atypical behavior,
2. value associated with atypical interaction (None if Boolean is False)
"""
exception = True
value_tot = 0.00
# needs to be this way since you want to find shortest distance first
atoms_coo = []
atoms_coo.extend(group_coo.get_interaction_atoms(group_arg))
atoms_arg = []
atoms_arg.extend(group_arg.get_interaction_atoms(group_coo))
for _ in ["shortest", "runner-up"]:
# find the closest interaction pair
[closest_coo_atom, dist, closest_arg_atom] = get_smallest_distance(atoms_coo,
atoms_arg)
[dpka_max, cutoff] = version.get_hydrogen_bond_parameters(closest_coo_atom,
closest_arg_atom)
# calculate and sum up interaction energy
f_angle = 1.00
if group_arg.type in version.parameters.angular_dependent_sidechain_interactions:
atom3 = closest_arg_atom.bonded_atoms[0]
dist, f_angle, _ = angle_distance_factors(closest_coo_atom,
closest_arg_atom,
atom3)
value = hydrogen_bond_energy(dist, dpka_max, cutoff, f_angle)
value_tot += value
# remove closest atoms before we attemp to find the runner-up pair
atoms_coo.remove(closest_coo_atom)
atoms_arg.remove(closest_arg_atom)
return exception, value_tot
def check_coo_coo_exception(group1, group2, version):
"""Check for COO-COO hydrogen-bond atypical interaction behavior.
Args:
group1: first group for check
group2: second group for check
version: version object
Returns:
1. Boolean indicating atypical behavior,
2. value associated with atypical interaction (None if Boolean is False)
"""
exception = True
interact_groups12 = group1.get_interaction_atoms(group2)
interact_groups21 = group2.get_interaction_atoms(group1)
[closest_atom1, dist, closest_atom2] = get_smallest_distance(interact_groups12,
interact_groups21)
[dpka_max, cutoff] = version.get_hydrogen_bond_parameters(closest_atom1,
closest_atom2)
f_angle = 1.00
value = hydrogen_bond_energy(dist, dpka_max, cutoff, f_angle)
weight = calculate_pair_weight(version.parameters, group1.num_volume, group2.num_volume)
value = value * (1.0 + weight)
return exception, value
def check_coo_his_exception(group1, group2, version):
"""Check for COO-HIS atypical interaction behavior
Args:
group1: first group for check
group2: second group for check
version: version object
Returns:
1. Boolean indicating atypical behavior,
2. value associated with atypical interaction (None if Boolean is False)
"""
exception = False
if check_buried(group1.num_volume, group2.num_volume):
exception = True
return exception, version.parameters.COO_HIS_exception
def check_oco_his_exception(group1, group2, version):
"""Check for OCO-HIS atypical interaction behavior
Args:
group1: first group for check
group2: second group for check
version: version object
Returns:
1. Boolean indicating atypical behavior,
2. value associated with atypical interaction (None if Boolean is False)
"""
exception = False
if check_buried(group1.num_volume, group2.num_volume):
exception = True
return exception, version.parameters.OCO_HIS_exception
def check_cys_his_exception(group1, group2, version):
"""Check for CYS-HIS atypical interaction behavior
Args:
group1: first group for check
group2: second group for check
version: version object
Returns:
1. Boolean indicating atypical behavior,
2. value associated with atypical interaction (None if Boolean is False)
"""
exception = False
if check_buried(group1.num_volume, group2.num_volume):
exception = True
return exception, version.parameters.CYS_HIS_exception
def check_cys_cys_exception(group1, group2, version):
"""Check for CYS-CYS atypical interaction behavior
Args:
group1: first group for check
group2: second group for check
version: version object
Returns:
1. Boolean indicating atypical behavior,
2. value associated with atypical interaction (None if Boolean is False)
"""
exception = False
if check_buried(group1.num_volume, group2.num_volume):
exception = True
return exception, version.parameters.CYS_CYS_exception
def check_buried(num_volume1, num_volume2):
"""Check to see if an interaction is buried
Args:
num_volume1: number of buried heavy atoms in volume 1
num_volume2: number of buried heavy atoms in volume 2
Returns:
True if interaction is buried, False otherwise
"""
if ((num_volume1 + num_volume2 <= COMBINED_NUM_BURIED_MAX)
and (num_volume1 <= SEPARATE_NUM_BURIED_MAX
or num_volume2 <= SEPARATE_NUM_BURIED_MAX)):
return False
return True

View File

@@ -6,6 +6,7 @@ from propka.ligand_pka_values import LigandPkaValues
from propka.determinant import Determinant from propka.determinant import Determinant
from propka.lib import info, warning from propka.lib import info, warning
# Constants that start with "UNK_" are a mystery to me # Constants that start with "UNK_" are a mystery to me
UNK_PKA_SCALING = -1.36 UNK_PKA_SCALING = -1.36
PROTONATOR = propka.protonate.Protonate(verbose=False) PROTONATOR = propka.protonate.Protonate(verbose=False)
@@ -1417,3 +1418,15 @@ def is_ion_group(parameters, atom):
if atom.res_name.strip() in parameters.ions.keys(): if atom.res_name.strip() in parameters.ions.keys():
return IonGroup(atom) return IonGroup(atom)
return None return None
def initialize_atom_group(atom):
"""Initialize an atom group.
Args:
atom: atom to initialize
"""
# try to initialise the group
group_attr = globals()[atom.group_label]
atom.group = group_attr(atom)
atom.group.model_pka = atom.group_model_pka
atom.group.model_pka_set = atom.group_model_pka_set

346
propka/hydrogens.py Normal file
View File

@@ -0,0 +1,346 @@
"""Calculations related to hydrogen placement."""
import math
from propka.lib import info
from propka.protonate import Protonate
from propka.bonds import BondMaker
from propka.atom import Atom
def setup_bonding_and_protonation(molecular_container):
"""Set up bonding and protonation for a molecule.
Args:
parameters: not used
molecular_container: molecule container.
"""
# make bonds
my_bond_maker = setup_bonding(molecular_container)
# set up ligand atom names
set_ligand_atom_names(molecular_container)
# apply information on pi electrons
my_bond_maker.add_pi_electron_information(molecular_container)
# Protonate atoms
if molecular_container.options.protonate_all:
protonator = Protonate(verbose=False)
protonator.protonate(molecular_container)
def setup_bonding(molecular_container):
"""Set up bonding for a molecular container.
Args:
molecular_container: the molecular container in question
Returns:
BondMaker object
"""
my_bond_maker = BondMaker()
my_bond_maker.find_bonds_for_molecules_using_boxes(molecular_container)
return my_bond_maker
def setup_bonding_and_protonation_30_style(molecular_container):
"""Set up bonding for a molecular container.
Args:
molecular_container: the molecular container in question
Returns:
BondMaker object
"""
# Protonate atoms
protonate_30_style(molecular_container)
# make bonds
bond_maker = BondMaker()
bond_maker.find_bonds_for_molecules_using_boxes(molecular_container)
return bond_maker
def protonate_30_style(molecular_container):
"""Protonate the molecule.
Args:
molecular_container: molecule
"""
for name in molecular_container.conformation_names:
info('Now protonating', name)
# split atom into residues
curres = -1000000
residue = []
o_atom = None
c_atom = None
for atom in molecular_container.conformations[name].atoms:
if atom.res_num != curres:
curres = atom.res_num
if len(residue) > 0:
#backbone
[o_atom, c_atom] = add_backbone_hydrogen(
residue, o_atom, c_atom)
#arginine
if residue[0].res_name == 'ARG':
add_arg_hydrogen(residue)
#histidine
if residue[0].res_name == 'HIS':
add_his_hydrogen(residue)
#tryptophan
if residue[0].res_name == 'TRP':
add_trp_hydrogen(residue)
#amides
if residue[0].res_name in ['GLN', 'ASN']:
add_amd_hydrogen(residue)
residue = []
if atom.type == 'atom':
residue.append(atom)
def set_ligand_atom_names(molecular_container):
"""Set names for ligands in molecular container.
Args:
molecular_container: molecular container for ligand names
"""
for name in molecular_container.conformation_names:
molecular_container.conformations[name].set_ligand_atom_names()
def add_arg_hydrogen(residue):
"""Adds Arg hydrogen atoms to residues according to the 'old way'.
Args:
residue: arginine residue to protonate
Returns:
list of hydrogen atoms
"""
#info('Adding arg H',residue)
for atom in residue:
if atom.name == "CD":
cd_atom = atom
elif atom.name == "CZ":
cz_atom = atom
elif atom.name == "NE":
ne_atom = atom
elif atom.name == "NH1":
nh1_atom = atom
elif atom.name == "NH2":
nh2_atom = atom
h1_atom = protonate_sp2(cd_atom, ne_atom, cz_atom)
h1_atom.name = "HE"
h2_atom = protonate_direction(nh1_atom, ne_atom, cz_atom)
h2_atom.name = "HN1"
h3_atom = protonate_direction(nh1_atom, ne_atom, cd_atom)
h3_atom.name = "HN2"
h4_atom = protonate_direction(nh2_atom, ne_atom, cz_atom)
h4_atom.name = "HN3"
h5_atom = protonate_direction(nh2_atom, ne_atom, h1_atom)
h5_atom.name = "HN4"
return [h1_atom, h2_atom, h3_atom, h4_atom, h5_atom]
def add_his_hydrogen(residue):
"""Adds His hydrogen atoms to residues according to the 'old way'.
Args:
residue: histidine residue to protonate
"""
for atom in residue:
if atom.name == "CG":
cg_atom = atom
elif atom.name == "ND1":
nd_atom = atom
elif atom.name == "CD2":
cd_atom = atom
elif atom.name == "CE1":
ce_atom = atom
elif atom.name == "NE2":
ne_atom = atom
hd_atom = protonate_sp2(cg_atom, nd_atom, ce_atom)
hd_atom.name = "HND"
he_atom = protonate_sp2(cd_atom, ne_atom, ce_atom)
he_atom.name = "HNE"
def add_trp_hydrogen(residue):
"""Adds Trp hydrogen atoms to residues according to the 'old way'.
Args:
residue: tryptophan residue to protonate
"""
cd_atom = None
ne_atom = None
for atom in residue:
if atom.name == "CD1":
cd_atom = atom
elif atom.name == "NE1":
ne_atom = atom
elif atom.name == "CE2":
ce_atom = atom
if (cd_atom is None) or (ne_atom is None) or (ce_atom is None):
str_ = "Unable to find all atoms for {0:s} {1:s}".format(
residue[0].res_name, residue[0].res_num)
raise ValueError(str_)
he_atom = protonate_sp2(cd_atom, ne_atom, ce_atom)
he_atom.name = "HNE"
def add_amd_hydrogen(residue):
"""Adds Gln & Asn hydrogen atoms to residues according to the 'old way'.
Args:
residue: glutamine or asparagine residue to protonate
"""
c_atom = None
o_atom = None
n_atom = None
for atom in residue:
if ((atom.res_name == "GLN" and atom.name == "CD")
or (atom.res_name == "ASN" and atom.name == "CG")):
c_atom = atom
elif ((atom.res_name == "GLN" and atom.name == "OE1")
or (atom.res_name == "ASN" and atom.name == "OD1")):
o_atom = atom
elif ((atom.res_name == "GLN" and atom.name == "NE2")
or (atom.res_name == "ASN" and atom.name == "ND2")):
n_atom = atom
if (c_atom is None) or (o_atom is None) or (n_atom is None):
str_ = "Unable to find all atoms for {0:s} {1:s}".format(
residue[0].res_name, residue[0].res_num)
raise ValueError(str_)
h1_atom = protonate_direction(n_atom, o_atom, c_atom)
h1_atom.name = "HN1"
h2_atom = protonate_average_direction(n_atom, c_atom, o_atom)
h2_atom.name = "HN2"
def add_backbone_hydrogen(residue, o_atom, c_atom):
"""Adds hydrogen backbone atoms to residues according to the old way.
dR is wrong for the N-terminus (i.e. first residue) but it doesn't affect
anything at the moment. Could be improved, but works for now.
Args:
residue: residue to protonate
o_atom: backbone oxygen atom
c_atom: backbone carbon atom
Returns:
[new backbone oxygen atom, new backbone carbon atom]
"""
new_c_atom = None
new_o_atom = None
n_atom = None
for atom in residue:
if atom.name == "N":
n_atom = atom
if atom.name == "C":
new_c_atom = atom
if atom.name == "O":
new_o_atom = atom
if None in [c_atom, o_atom, n_atom]:
return [new_o_atom, new_c_atom]
if n_atom.res_name == "PRO":
# PRO doesn't have an H-atom; do nothing
pass
else:
h_atom = protonate_direction(n_atom, o_atom, c_atom)
h_atom.name = "H"
return [new_o_atom, new_c_atom]
def protonate_direction(x1_atom, x2_atom, x3_atom):
"""Protonates an atom, x1_atom, given a direction.
New direction for x1_atom proton is (x2_atom -> x3_atom).
Args:
x1_atom: atom to be protonated
x2_atom: atom for direction
x3_atom: other atom for direction
Returns:
new hydrogen atom
"""
dx = (x3_atom.x - x2_atom.x)
dy = (x3_atom.y - x2_atom.y)
dz = (x3_atom.z - x2_atom.z)
length = math.sqrt(dx*dx + dy*dy + dz*dz)
x = x1_atom.x + dx/length
y = x1_atom.y + dy/length
z = x1_atom.z + dz/length
h_atom = make_new_h(x1_atom, x, y, z)
h_atom.name = "H"
return h_atom
def protonate_average_direction(x1_atom, x2_atom, x3_atom):
"""Protonates an atom, x1_atom, given a direction.
New direction for x1_atom is (x1_atom/x2_atom -> x3_atom).
Note, this one uses the average of x1_atom & x2_atom (N & O) unlike
the previous N - C = O
Args:
x1_atom: atom to be protonated
x2_atom: atom for direction
x3_atom: other atom for direction
Returns:
new hydrogen atom
"""
dx = (x3_atom.x + x1_atom.x)*0.5 - x2_atom.x
dy = (x3_atom.y + x1_atom.y)*0.5 - x2_atom.y
dz = (x3_atom.z + x1_atom.z)*0.5 - x2_atom.z
length = math.sqrt(dx*dx + dy*dy + dz*dz)
x = x1_atom.x + dx/length
y = x1_atom.y + dy/length
z = x1_atom.z + dz/length
h_atom = make_new_h(x1_atom, x, y, z)
h_atom.name = "H"
return h_atom
def protonate_sp2(x1_atom, x2_atom, x3_atom):
"""Protonates a SP2 atom, given a list of atoms
Args:
x1_atom: atom to set direction
x2_atom: atom to be protonated
x3_atom: other atom to set direction
Returns:
new hydrogen atom
"""
dx = (x1_atom.x + x3_atom.x)*0.5 - x2_atom.x
dy = (x1_atom.y + x3_atom.y)*0.5 - x2_atom.y
dz = (x1_atom.z + x3_atom.z)*0.5 - x2_atom.z
length = math.sqrt(dx*dx + dy*dy + dz*dz)
x = x2_atom.x - dx/length
y = x2_atom.y - dy/length
z = x2_atom.z - dz/length
h_atom = make_new_h(x2_atom, x, y, z)
h_atom.name = "H"
return h_atom
def make_new_h(atom, x, y, z):
"""Add a new hydrogen to an atom at the specified position.
Args:
atom: atom to protonate
x: x position of hydrogen
y: y position of hydrogen
z: z position of hydrogen
Returns:
new hydrogen atom
"""
new_h = Atom()
new_h.set_property(
numb=None, name='H{0:s}'.format(atom.name[1:]),
res_name=atom.res_name, chain_id=atom.chain_id,
res_num=atom.res_num, x=x, y=y, z=z, occ=None, beta=None)
new_h.element = 'H'
new_h.bonded_atoms = [atom]
new_h.charge = 0
new_h.steric_number = 0
new_h.number_of_lone_pairs = 0
new_h.number_of_protons_to_add = 0
new_h.num_pi_elec_2_3_bonds = 0
atom.bonded_atoms.append(new_h)
atom.conformation_container.add_atom(new_h)
return new_h

View File

@@ -1,95 +1,121 @@
"""PDB parsing functionality.""" """Input routines."""
import propka.lib from pathlib import Path
from propka.lib import warning from pkg_resources import resource_filename
from propka.lib import protein_precheck
from propka.output import write_propka
from propka.atom import Atom from propka.atom import Atom
from propka.conformation_container import ConformationContainer from propka.conformation_container import ConformationContainer
from propka.group import initialize_atom_group
EXPECTED_ATOM_NUMBERS = {'ALA': 5, 'ARG': 11, 'ASN': 8, 'ASP': 8, 'CYS': 6, def open_file_for_reading(input_file):
'GLY': 4, 'GLN': 9, 'GLU': 9, 'HIS': 10, 'ILE': 8, """Open file or file-like stream for reading.
'LEU': 8, 'LYS': 9, 'MET': 8, 'PHE': 11, 'PRO': 7,
'SER': 6, 'THR': 7, 'TRP': 14, 'TYR': 12, 'VAL': 7}
TODO - convert this to a context manager
def read_pdb(pdb_file, parameters, molecule):
"""Parse a PDB file.
Args: Args:
pdb_file: file to read input_file: path to file or file-like object. If file-like object,
parameters: parameters to guide parsing then will attempt fseek(0).
molecule: molecular container
Returns:
list with elements:
1. list of conformations
2. list of names
""" """
conformations = {} try:
# read in all atoms in the file input_file.fseek(0)
lines = get_atom_lines_from_pdb( return input_file
pdb_file, ignore_residues=parameters.ignore_residues, except AttributeError:
keep_protons=molecule.options.keep_protons, pass
chains=molecule.options.chains)
for (name, atom) in lines: try:
if not name in conformations.keys(): file_ = open(input_file, 'rt')
conformations[name] = ConformationContainer( except:
name=name, parameters=parameters, molecular_container=molecule) raise IOError('Cannot find file {0:s}'.format(input_file))
conformations[name].add_atom(atom) return file_
# make a sorted list of conformation names
names = sorted(conformations.keys(), key=propka.lib.conformation_sorter)
return [conformations, names]
def protein_precheck(conformations, names): def read_molecule_file(input_file, mol_container):
"""Check protein for correct number of atoms, etc. """Read input file (PDB or PROPKA) for a molecular container
Args: Args
names: conformation names to check input_file: input file to read
""" mol_container: MolecularContainer object
for name in names:
atoms = conformations[name].atoms
# Group the atoms by their residue:
atoms_by_residue = {}
for atom in atoms:
if atom.element != 'H':
res_id = resid_from_atom(atom)
try:
atoms_by_residue[res_id].append(atom)
except KeyError:
atoms_by_residue[res_id] = [atom]
for res_id, res_atoms in atoms_by_residue.items():
res_name = res_atoms[0].res_name
residue_label = '{0:>3s}{1:>5s}'.format(res_name, res_id)
# ignore ligand residues
if res_name not in EXPECTED_ATOM_NUMBERS:
continue
# check for c-terminal
if 'C-' in [a.terminal for a in res_atoms]:
if len(res_atoms) != EXPECTED_ATOM_NUMBERS[res_name]+1:
str_ = ("Unexpected number ({num:d}) of atoms in residue "
"{res:s} in conformation {conf:s}".format(
num=len(res_atoms), res=residue_label,
conf=name))
warning(str_)
continue
# check number of atoms in residue
if len(res_atoms) != EXPECTED_ATOM_NUMBERS[res_name]:
str_ = ("Unexpected number ({num:d}) of atoms in residue "
"{res:s} in conformation {conf:s}".format(
num=len(res_atoms), res=residue_label,
conf=name))
warning(str_)
def resid_from_atom(atom):
"""Return string with atom residue information.
Args:
atom: atom to generate string for
Returns Returns
string updated MolecularContainer object
Raises
ValuError if invalid input given
""" """
return '{0:>4d} {1:s} {2:s}'.format( input_path = Path(input_file)
atom.res_num, atom.chain_id, atom.icode) mol_container.name = input_path.stem
input_file_extension = input_path.suffix
if input_file_extension.lower() == '.pdb':
# input is a pdb file. read in atoms and top up containers to make
# sure that all atoms are present in all conformations
conformations, conformation_names = read_pdb(
input_path, mol_container.version.parameters, mol_container)
if len(conformations) == 0:
str_ = ('Error: The pdb file does not seems to contain any '
'molecular conformations')
raise ValueError(str_)
mol_container.conformations = conformations
mol_container.conformation_names = conformation_names
mol_container.top_up_conformations()
# make a structure precheck
protein_precheck(
mol_container.conformations, mol_container.conformation_names)
# set up atom bonding and protonation
mol_container.version.setup_bonding_and_protonation(mol_container)
# Extract groups
mol_container.extract_groups()
# sort atoms
for name in mol_container.conformation_names:
mol_container.conformations[name].sort_atoms()
# find coupled groups
mol_container.find_covalently_coupled_groups()
# write out the input file
# TODO - figure out why this I/O has to happen here
output_path = Path(input_path.name.replace(
input_file_extension, '.propka_input'))
write_propka(mol_container, output_path)
elif input_file_extension.lower() == '.propka_input':
# input is a propka_input file
conformations, conformation_names = read_propka(
input_file, mol_container.version.parameters, mol_container)
mol_container.conformations = conformations
mol_container.conformation_names = conformation_names
# Extract groups - this merely sets up the groups found in the
# input file
mol_container.extract_groups()
# do some additional set up
mol_container.additional_setup_when_reading_input_file()
else:
str_ = "Unknown input file type {0!s} for file {1!s}".format(
input_file_extension, input_path)
raise ValueError(str_)
return mol_container
def read_parameter_file(input_file, parameters):
"""Read a parameter file.
Args:
input_file: input file to read
parameters: Parameters object
Returns:
updated Parameters object
"""
# try to locate the parameter file
try:
ifile = resource_filename(__name__, input_file)
input_ = open_file_for_reading(ifile)
except (IOError, FileNotFoundError, ValueError):
input_ = open_file_for_reading(input_file)
for line in input_:
parameters.parse_line(line)
return parameters
def conformation_sorter(conf):
"""TODO - figure out what this function does."""
model = int(conf[:-1])
altloc = conf[-1:]
return model*100+ord(altloc)
def get_atom_lines_from_pdb(pdb_file, ignore_residues=[], keep_protons=False, def get_atom_lines_from_pdb(pdb_file, ignore_residues=[], keep_protons=False,
@@ -103,7 +129,7 @@ def get_atom_lines_from_pdb(pdb_file, ignore_residues=[], keep_protons=False,
tags: tags of lines that include atoms tags: tags of lines that include atoms
chains: list of chains chains: list of chains
""" """
lines = propka.lib.open_file_for_reading(pdb_file).readlines() lines = open_file_for_reading(pdb_file).readlines()
nterm_residue = 'next_residue' nterm_residue = 'next_residue'
old_residue = None old_residue = None
terminal = None terminal = None
@@ -158,119 +184,7 @@ def get_atom_lines_from_pdb(pdb_file, ignore_residues=[], keep_protons=False,
terminal = None terminal = None
def write_pdb(conformation, filename): def read_propka(input_file, parameters, molecule):
"""Write PDB conformation to a file.
Args:
conformation: conformation container
filename: filename for output
"""
write_pdb_for_atoms(conformation.atoms, filename)
def write_pdb_for_atoms(atoms, filename, make_conect_section=False):
"""Write out PDB file for atoms.
Args:
atoms: list of atoms
filename: name of file
make_conect_section: generate a CONECT PDB section
"""
out = propka.lib.open_file_for_writing(filename)
for atom in atoms:
out.write(atom.make_pdb_line())
if make_conect_section:
for atom in atoms:
out.write(atom.make_conect_line())
out.close()
def write_mol2_for_atoms(atoms, filename):
"""Write out MOL2 file for atoms.
Args:
atoms: list of atoms
filename: name of file
"""
# TODO - header needs to be converted to format string
header = '@<TRIPOS>MOLECULE\n\n{natom:d} {id:d}\nSMALL\nUSER_CHARGES\n'
atoms_section = '@<TRIPOS>ATOM\n'
for i, atom in enumerate(atoms):
atoms_section += atom.make_mol2_line(i+1)
bonds_section = '@<TRIPOS>BOND\n'
id_ = 1
for i, atom1 in enumerate(atoms):
for j, atom2 in enumerate(atoms, i+1):
if atom1 in atom2.bonded_atoms:
type_ = get_bond_order(atom1, atom2)
bonds_section += '{0:>7d} {1:>7d} {2:>7d} {3:>7s}\n'.format(
id_, i+1, j+1, type_)
id_ += 1
substructure_section = '@<TRIPOS>SUBSTRUCTURE\n\n'
if len(atoms) > 0:
substructure_section = (
'@<TRIPOS>SUBSTRUCTURE\n{0:<7d} {1:>10s} {2:>7d}\n'.format(
atoms[0].res_num, atoms[0].res_name, atoms[0].numb))
out = propka.lib.open_file_for_writing(filename)
out.write(header.format(natom=len(atoms), id=id_-1))
out.write(atoms_section)
out.write(bonds_section)
out.write(substructure_section)
out.close()
def get_bond_order(atom1, atom2):
"""Get the order of a bond between two atoms.
Args:
atom1: first atom in bond
atom2: second atom in bond
Returns:
string with bond type
"""
type_ = '1'
pi_electrons1 = atom1.num_pi_elec_2_3_bonds
pi_electrons2 = atom2.num_pi_elec_2_3_bonds
if '.ar' in atom1.sybyl_type:
pi_electrons1 -= 1
if '.ar' in atom2.sybyl_type:
pi_electrons2 -= 1
if pi_electrons1 > 0 and pi_electrons2 > 0:
type_ = '{0:d}'.format(min(pi_electrons1, pi_electrons2)+1)
if '.ar' in atom1.sybyl_type and '.ar' in atom2.sybyl_type:
type_ = 'ar'
return type_
def write_input(molecular_container, filename):
"""Write PROPKA input file for molecular container.
Args:
molecular_container: molecular container
filename: output file name
"""
out = propka.lib.open_file_for_writing(filename)
for conformation_name in molecular_container.conformation_names:
out.write('MODEL {0:s}\n'.format(conformation_name))
# write atoms
for atom in molecular_container.conformations[conformation_name].atoms:
out.write(atom.make_input_line())
# write bonds
for atom in molecular_container.conformations[conformation_name].atoms:
out.write(atom.make_conect_line())
# write covalently coupled groups
for group in (
molecular_container.conformations[conformation_name].groups):
out.write(group.make_covalently_coupled_line())
# write non-covalently coupled groups
for group in (
molecular_container.conformations[conformation_name].groups):
out.write(group.make_non_covalently_coupled_line())
out.write('ENDMDL\n')
out.close()
def read_input(input_file, parameters, molecule):
"""Read PROPKA input file for molecular container. """Read PROPKA input file for molecular container.
Args: Args:
@@ -290,7 +204,7 @@ def read_input(input_file, parameters, molecule):
molecular_container=molecule) molecular_container=molecule)
conformations[name].add_atom(atom) conformations[name].add_atom(atom)
# make a sorted list of conformation names # make a sorted list of conformation names
names = sorted(conformations.keys(), key=propka.lib.conformation_sorter) names = sorted(conformations.keys(), key=conformation_sorter)
return [conformations, names] return [conformations, names]
@@ -303,7 +217,7 @@ def get_atom_lines_from_input(input_file, tags=['ATOM ', 'HETATM']):
Yields: Yields:
conformation container, list of atoms conformation container, list of atoms
""" """
lines = propka.lib.open_file_for_reading(input_file).readlines() lines = open_file_for_reading(input_file).readlines()
conformation = '' conformation = ''
atoms = {} atoms = {}
numbers = [] numbers = []
@@ -316,6 +230,7 @@ def get_atom_lines_from_input(input_file, tags=['ATOM ', 'HETATM']):
if tag in tags: if tag in tags:
atom = Atom(line=line) atom = Atom(line=line)
atom.get_input_parameters() atom.get_input_parameters()
initialize_atom_group(atom)
atom.groups_extracted = 1 atom.groups_extracted = 1
atom.is_protonated = True atom.is_protonated = True
atoms[atom.numb] = atom atoms[atom.numb] = atom
@@ -356,3 +271,30 @@ def get_atom_lines_from_input(input_file, tags=['ATOM ', 'HETATM']):
# prepare for next conformation # prepare for next conformation
atoms = {} atoms = {}
numbers = [] numbers = []
def read_pdb(pdb_file, parameters, molecule):
"""Parse a PDB file.
Args:
pdb_file: file to read
parameters: parameters to guide parsing
molecule: molecular container
Returns:
list with elements:
1. list of conformations
2. list of names
"""
conformations = {}
# read in all atoms in the file
lines = get_atom_lines_from_pdb(
pdb_file, ignore_residues=parameters.ignore_residues,
keep_protons=molecule.options.keep_protons,
chains=molecule.options.chains)
for (name, atom) in lines:
if not name in conformations.keys():
conformations[name] = ConformationContainer(
name=name, parameters=parameters, molecular_container=molecule)
conformations[name].add_atom(atom)
# make a sorted list of conformation names
names = sorted(conformations.keys(), key=conformation_sorter)
return [conformations, names]

View File

@@ -11,56 +11,63 @@ _STDOUT_HANDLER.setFormatter(logging.Formatter("%(message)s"))
_LOGGER.addHandler(_STDOUT_HANDLER) _LOGGER.addHandler(_STDOUT_HANDLER)
def open_file_for_reading(input_file): EXPECTED_ATOM_NUMBERS = {'ALA': 5, 'ARG': 11, 'ASN': 8, 'ASP': 8, 'CYS': 6,
"""Open file or file-like stream for reading. 'GLY': 4, 'GLN': 9, 'GLU': 9, 'HIS': 10, 'ILE': 8,
'LEU': 8, 'LYS': 9, 'MET': 8, 'PHE': 11, 'PRO': 7,
'SER': 6, 'THR': 7, 'TRP': 14, 'TYR': 12, 'VAL': 7}
TODO - convert this to a context manager
def protein_precheck(conformations, names):
"""Check protein for correct number of atoms, etc.
Args: Args:
input_file: path to file or file-like object. If file-like object, names: conformation names to check
then will attempt fseek(0).
""" """
try: for name in names:
input_file.fseek(0) atoms = conformations[name].atoms
return input_file # Group the atoms by their residue:
except AttributeError: atoms_by_residue = {}
pass for atom in atoms:
if atom.element != 'H':
try: res_id = resid_from_atom(atom)
file_ = open(input_file, 'rt') try:
except: atoms_by_residue[res_id].append(atom)
raise IOError('Cannot find file {0:s}'.format(input_file)) except KeyError:
return file_ atoms_by_residue[res_id] = [atom]
for res_id, res_atoms in atoms_by_residue.items():
res_name = res_atoms[0].res_name
residue_label = '{0:>3s}{1:>5s}'.format(res_name, res_id)
# ignore ligand residues
if res_name not in EXPECTED_ATOM_NUMBERS:
continue
# check for c-terminal
if 'C-' in [a.terminal for a in res_atoms]:
if len(res_atoms) != EXPECTED_ATOM_NUMBERS[res_name]+1:
str_ = ("Unexpected number ({num:d}) of atoms in residue "
"{res:s} in conformation {conf:s}".format(
num=len(res_atoms), res=residue_label,
conf=name))
warning(str_)
continue
# check number of atoms in residue
if len(res_atoms) != EXPECTED_ATOM_NUMBERS[res_name]:
str_ = ("Unexpected number ({num:d}) of atoms in residue "
"{res:s} in conformation {conf:s}".format(
num=len(res_atoms), res=residue_label,
conf=name))
warning(str_)
def open_file_for_writing(input_file): def resid_from_atom(atom):
"""Open file or file-like stream for writing. """Return string with atom residue information.
TODO - convert this to a context manager.
Args: Args:
input_file: path to file or file-like object. If file-like object, atom: atom to generate string for
then will attempt to get file mode. Returns
string
""" """
try: return '{0:>4d} {1:s} {2:s}'.format(
mode = input_file.mode atom.res_num, atom.chain_id, atom.icode)
if not ("w" in mode or "a" in mode or "+" in mode):
raise IOError("File/stream not open for writing")
return input_file
except AttributeError:
pass
try:
file_ = open(input_file, 'wt')
except FileNotFoundError:
raise Exception('Could not open {0:s}'.format(input_file))
return file_
def conformation_sorter(conf):
"""TODO - figure out what this function does."""
model = int(conf[:-1])
altloc = conf[-1:]
return model*100+ord(altloc)
def split_atoms_into_molecules(atoms): def split_atoms_into_molecules(atoms):
@@ -354,19 +361,6 @@ def configuration_compare(conf):
return 100*int(conf[1:-2]) + ord(conf[-1]) return 100*int(conf[1:-2]) + ord(conf[-1])
def write_file(filename, lines):
"""Writes a new file.
Args:
filename: name of file
lines: lines to write to file
"""
file_ = open_file_for_writing(filename)
for line in lines:
file_.write("{0:s}\n".format(line))
file_.close()
def _args_to_str(arg_list): def _args_to_str(arg_list):
"""Summarize list of arguments in string. """Summarize list of arguments in string.

View File

@@ -2,12 +2,8 @@
import os import os
import subprocess import subprocess
import sys import sys
import propka.molecular_container from propka.output import write_mol2_for_atoms
import propka.calculations from propka.lib import info, warning, split_atoms_into_molecules
import propka.parameters
import propka.pdb
import propka.lib
from propka.lib import info, warning
class LigandPkaValues: class LigandPkaValues:
@@ -48,17 +44,17 @@ class LigandPkaValues:
sys.exit(-1) sys.exit(-1)
return locs[0] return locs[0]
def get_marvin_pkas_for_pdb_file(self, pdbfile, num_pkas=10, min_ph=-10, def get_marvin_pkas_for_pdb_file(
max_ph=20): self, molecule, parameters, num_pkas=10, min_ph=-10, max_ph=20):
"""Use Marvin executables to get pKas for a PDB file. """Use Marvin executables to get pKas for a PDB file.
Args: Args:
pdbfile: PDB file pdbfile: PDB file
molecule: MolecularContainer object
num_pkas: number of pKas to get num_pkas: number of pKas to get
min_ph: minimum pH value min_ph: minimum pH value
max_ph: maximum pH value max_ph: maximum pH value
""" """
molecule = propka.molecular_container.Molecular_container(pdbfile)
self.get_marvin_pkas_for_molecular_container( self.get_marvin_pkas_for_molecular_container(
molecule, num_pkas=num_pkas, min_ph=min_ph, max_ph=max_ph) molecule, num_pkas=num_pkas, min_ph=min_ph, max_ph=max_ph)
@@ -111,7 +107,7 @@ class LigandPkaValues:
max_ph: maximum pH value max_ph: maximum pH value
""" """
# do one molecule at the time so we don't confuse marvin # do one molecule at the time so we don't confuse marvin
molecules = propka.lib.split_atoms_into_molecules(atoms) molecules = split_atoms_into_molecules(atoms)
for i, molecule in enumerate(molecules): for i, molecule in enumerate(molecules):
filename = '{0:s}_{1:d}.mol2'.format(name, i+1) filename = '{0:s}_{1:d}.mol2'.format(name, i+1)
self.get_marvin_pkas_for_molecule( self.get_marvin_pkas_for_molecule(
@@ -133,7 +129,7 @@ class LigandPkaValues:
""" """
# print out structure unless we are using user-modified structure # print out structure unless we are using user-modified structure
if not reuse: if not reuse:
propka.pdb.write_mol2_for_atoms(atoms, filename) write_mol2_for_atoms(atoms, filename)
# check that we actually have a file to work with # check that we actually have a file to work with
if not os.path.isfile(filename): if not os.path.isfile(filename):
errstr = ( errstr = (
@@ -141,7 +137,7 @@ class LigandPkaValues:
"- generating one".format( "- generating one".format(
filename)) filename))
warning(errstr) warning(errstr)
propka.pdb.write_mol2_for_atoms(atoms, filename) write_mol2_for_atoms(atoms, filename)
# Marvin calculate pKa values # Marvin calculate pKa values
fmt = ( fmt = (
'pka -a {num1} -b {num2} --min {min_ph} ' 'pka -a {num1} -b {num2} --min {min_ph} '

View File

@@ -1,13 +1,8 @@
"""Molecular container for storing all contents of PDB files.""" """Molecular container for storing all contents of PDB files."""
import os import os
import sys
import propka.pdb
import propka.version import propka.version
import propka.output
import propka.group
import propka.lib
from propka.conformation_container import ConformationContainer from propka.conformation_container import ConformationContainer
from propka.lib import info, warning from propka.lib import info, warning, make_grid
# TODO - these are constants whose origins are a little murky # TODO - these are constants whose origins are a little murky
@@ -16,35 +11,26 @@ UNK_PI_CUTOFF = 0.01
MAX_ITERATION = 4 MAX_ITERATION = 4
class Molecular_container: class MolecularContainer:
"""Container for storing molecular contents of PDB files. """Container for storing molecular contents of PDB files.
TODO - this class name does not conform to PEP8 but has external use. TODO - this class name does not conform to PEP8 but has external use.
We should deprecate and change eventually. We should deprecate and change eventually.
""" """
def __init__(self, input_file, options=None): def __init__(self, parameters, options=None):
"""Initialize molecular container. """Initialize molecular container.
Args: Args:
input_file: molecular input file parameters: Parameters() object
options: options object options: options object
""" """
# printing out header before parsing input # printing out header before parsing input
propka.output.print_header() propka.output.print_header()
# set up some values self.conformation_names = []
self.conformations = {}
self.options = options self.options = options
self.input_file = input_file self.name = None
# TODO - replace this indelicate os.path code with pathlib
self.dir = os.path.split(input_file)[0]
self.file = os.path.split(input_file)[1]
self.name = self.file[0:self.file.rfind('.')]
input_file_extension = input_file[input_file.rfind('.'):]
# set the version
if options:
parameters = propka.parameters.Parameters(self.options.parameters)
else:
parameters = propka.parameters.Parameters('propka.cfg')
try: try:
version_class = getattr(propka.version, parameters.version) version_class = getattr(propka.version, parameters.version)
self.version = version_class(parameters) self.version = version_class(parameters)
@@ -53,45 +39,6 @@ class Molecular_container:
errstr = 'Error: Version {0:s} does not exist'.format( errstr = 'Error: Version {0:s} does not exist'.format(
parameters.version) parameters.version)
raise Exception(errstr) raise Exception(errstr)
# read the input file
if input_file_extension[0:4] == '.pdb':
# input is a pdb file. read in atoms and top up containers to make
# sure that all atoms are present in all conformations
[self.conformations, self.conformation_names] = (
propka.pdb.read_pdb(input_file, self.version.parameters, self))
if len(self.conformations) == 0:
info('Error: The pdb file does not seems to contain any '
'molecular conformations')
sys.exit(-1)
self.top_up_conformations()
# make a structure precheck
propka.pdb.protein_precheck(self.conformations,
self.conformation_names)
# set up atom bonding and protonation
self.version.setup_bonding_and_protonation(self)
# Extract groups
self.extract_groups()
# sort atoms
for name in self.conformation_names:
self.conformations[name].sort_atoms()
# find coupled groups
self.find_covalently_coupled_groups()
# write out the input file
filename = self.file.replace(input_file_extension, '.propka_input')
propka.pdb.write_input(self, filename)
elif input_file_extension == '.propka_input':
#input is a propka_input file
[self.conformations, self.conformation_names] = (
propka.pdb.read_input(input_file, self.version.parameters,
self))
# Extract groups - this merely sets up the groups found in the
# input file
self.extract_groups()
# do some additional set up
self.additional_setup_when_reading_input_file()
else:
info('Unrecognized input file:{0:s}'.format(input_file))
sys.exit(-1)
def top_up_conformations(self): def top_up_conformations(self):
"""Makes sure that all atoms are present in all conformations.""" """Makes sure that all atoms are present in all conformations."""
@@ -155,7 +102,7 @@ class Molecular_container:
else: else:
str_ = ( str_ = (
'Group {0:s} could not be found in ' 'Group {0:s} could not be found in '
'conformation {0:s}.'.format( 'conformation {1:s}.'.format(
group.atom.residue_label, name)) group.atom.residue_label, name))
warning(str_) warning(str_)
# ... and store the average value # ... and store the average value
@@ -214,7 +161,7 @@ class Molecular_container:
""" """
# calculate stability profile # calculate stability profile
profile = [] profile = []
for ph in propka.lib.make_grid(*grid): for ph in make_grid(*grid):
conf = self.conformations[conformation] conf = self.conformations[conformation]
ddg = conf.calculate_folding_energy(ph=ph, reference=reference) ddg = conf.calculate_folding_energy(ph=ph, reference=reference)
profile.append([ph, ddg]) profile.append([ph, ddg])
@@ -244,7 +191,7 @@ class Molecular_container:
list of charge state values list of charge state values
""" """
charge_profile = [] charge_profile = []
for ph in propka.lib.make_grid(*grid): for ph in make_grid(*grid):
conf = self.conformations[conformation] conf = self.conformations[conformation]
q_unfolded, q_folded = conf.calculate_charge( q_unfolded, q_folded = conf.calculate_charge(
self.version.parameters, ph=ph) self.version.parameters, ph=ph)

View File

@@ -3,6 +3,41 @@ from datetime import date
from propka.lib import info from propka.lib import info
def open_file_for_writing(input_file):
"""Open file or file-like stream for writing.
TODO - convert this to a context manager.
Args:
input_file: path to file or file-like object. If file-like object,
then will attempt to get file mode.
"""
try:
if not input_file.writable():
raise IOError("File/stream not open for writing")
return input_file
except AttributeError:
pass
try:
file_ = open(input_file, 'wt')
except FileNotFoundError:
raise Exception('Could not open {0:s}'.format(input_file))
return file_
def write_file(filename, lines):
"""Writes a new file.
Args:
filename: name of file
lines: lines to write to file
"""
file_ = open_file_for_writing(filename)
for line in lines:
file_.write("{0:s}\n".format(line))
file_.close()
def print_header(): def print_header():
"""Print header section of output.""" """Print header section of output."""
str_ = "{0:s}\n".format(get_propka_header()) str_ = "{0:s}\n".format(get_propka_header())
@@ -11,8 +46,8 @@ def print_header():
info(str_) info(str_)
def write_pdb(protein, pdbfile=None, filename=None, include_hydrogens=False, def write_pdb_for_protein(
_=None): protein, pdbfile=None, filename=None, include_hydrogens=False, _=None):
"""Write a residue to the new PDB file. """Write a residue to the new PDB file.
Args: Args:
@@ -50,6 +85,16 @@ def write_pdb(protein, pdbfile=None, filename=None, include_hydrogens=False,
pdbfile.close() pdbfile.close()
def write_pdb_for_conformation(conformation, filename):
"""Write PDB conformation to a file.
Args:
conformation: conformation container
filename: filename for output
"""
write_pdb_for_atoms(conformation.atoms, filename)
def write_pka(protein, parameters, filename=None, conformation='1A', def write_pka(protein, parameters, filename=None, conformation='1A',
reference="neutral", _="folding", verbose=False, reference="neutral", _="folding", verbose=False,
__=None): __=None):
@@ -204,8 +249,8 @@ def get_summary_section(protein, conformation, parameters):
def get_folding_profile_section( def get_folding_profile_section(
protein, conformation='AVR', direction="folding", reference="neutral", protein, conformation='AVR', direction="folding", reference="neutral",
window=[0., 14., 1.0], _=False, __=None): window=[0., 14., 1.0], _=False, __=None):
"""Returns string with the folding profile section of the results. """Returns string with the folding profile section of the results.
Args: Args:
@@ -461,3 +506,104 @@ def make_interaction_map(name, list_, interaction):
tag = ' X ' tag = ' X '
res += '{0:>10s}| '.format(tag) res += '{0:>10s}| '.format(tag)
return res return res
def write_pdb_for_atoms(atoms, filename, make_conect_section=False):
"""Write out PDB file for atoms.
Args:
atoms: list of atoms
filename: name of file
make_conect_section: generate a CONECT PDB section
"""
out = open_file_for_writing(filename)
for atom in atoms:
out.write(atom.make_pdb_line())
if make_conect_section:
for atom in atoms:
out.write(atom.make_conect_line())
out.close()
def get_bond_order(atom1, atom2):
"""Get the order of a bond between two atoms.
Args:
atom1: first atom in bond
atom2: second atom in bond
Returns:
string with bond type
"""
type_ = '1'
pi_electrons1 = atom1.num_pi_elec_2_3_bonds
pi_electrons2 = atom2.num_pi_elec_2_3_bonds
if '.ar' in atom1.sybyl_type:
pi_electrons1 -= 1
if '.ar' in atom2.sybyl_type:
pi_electrons2 -= 1
if pi_electrons1 > 0 and pi_electrons2 > 0:
type_ = '{0:d}'.format(min(pi_electrons1, pi_electrons2)+1)
if '.ar' in atom1.sybyl_type and '.ar' in atom2.sybyl_type:
type_ = 'ar'
return type_
def write_mol2_for_atoms(atoms, filename):
"""Write out MOL2 file for atoms.
Args:
atoms: list of atoms
filename: name of file
"""
# TODO - header needs to be converted to format string
header = '@<TRIPOS>MOLECULE\n\n{natom:d} {id:d}\nSMALL\nUSER_CHARGES\n'
atoms_section = '@<TRIPOS>ATOM\n'
for i, atom in enumerate(atoms):
atoms_section += atom.make_mol2_line(i+1)
bonds_section = '@<TRIPOS>BOND\n'
id_ = 1
for i, atom1 in enumerate(atoms):
for j, atom2 in enumerate(atoms, i+1):
if atom1 in atom2.bonded_atoms:
type_ = get_bond_order(atom1, atom2)
bonds_section += '{0:>7d} {1:>7d} {2:>7d} {3:>7s}\n'.format(
id_, i+1, j+1, type_)
id_ += 1
substructure_section = '@<TRIPOS>SUBSTRUCTURE\n\n'
if len(atoms) > 0:
substructure_section = (
'@<TRIPOS>SUBSTRUCTURE\n{0:<7d} {1:>10s} {2:>7d}\n'.format(
atoms[0].res_num, atoms[0].res_name, atoms[0].numb))
out = open_file_for_writing(filename)
out.write(header.format(natom=len(atoms), id=id_-1))
out.write(atoms_section)
out.write(bonds_section)
out.write(substructure_section)
out.close()
def write_propka(molecular_container, filename):
"""Write PROPKA input file for molecular container.
Args:
molecular_container: molecular container
filename: output file name
"""
out = open_file_for_writing(filename)
for conformation_name in molecular_container.conformation_names:
out.write('MODEL {0:s}\n'.format(conformation_name))
# write atoms
for atom in molecular_container.conformations[conformation_name].atoms:
out.write(atom.make_input_line())
# write bonds
for atom in molecular_container.conformations[conformation_name].atoms:
out.write(atom.make_conect_line())
# write covalently coupled groups
for group in (
molecular_container.conformations[conformation_name].groups):
out.write(group.make_covalently_coupled_line())
# write non-covalently coupled groups
for group in (
molecular_container.conformations[conformation_name].groups):
out.write(group.make_non_covalently_coupled_line())
out.write('ENDMDL\n')
out.close()

View File

@@ -1,6 +1,4 @@
"""Holds parameters and settings.""" """Holds parameters and settings."""
import pkg_resources
import propka.lib as lib
from propka.lib import info, warning from propka.lib import info, warning
@@ -35,7 +33,7 @@ STRINGS = ['version', 'output_file_tag', 'ligand_typing', 'pH', 'reference']
class Parameters: class Parameters:
"""PROPKA parameter class.""" """PROPKA parameter class."""
def __init__(self, parameter_file): def __init__(self):
"""Initialize parameter class. """Initialize parameter class.
Args: Args:
@@ -52,22 +50,6 @@ class Parameters:
self.CYS_CYS_exception = None self.CYS_CYS_exception = None
# These functions set up remaining data structures implicitly # These functions set up remaining data structures implicitly
self.set_up_data_structures() self.set_up_data_structures()
self.read_parameters(parameter_file)
def read_parameters(self, file_):
"""Read parameters from file.
Args:
file_: file to read
"""
# try to locate the parameters file
try:
ifile = pkg_resources.resource_filename(__name__, file_)
input_ = lib.open_file_for_reading(ifile)
except (IOError, FileNotFoundError, ValueError):
input_ = lib.open_file_for_reading(file_)
for line in input_:
self.parse_line(line)
def parse_line(self, line): def parse_line(self, line):
"""Parse parameter file line.""" """Parse parameter file line."""
@@ -362,7 +344,7 @@ O2
'N1', 'O2', 'OP', 'SH'] 'N1', 'O2', 'OP', 'SH']
lines = [ lines = [
"", "",
"\\begin{longtable}{{{0:s}}}".format('l'*len(agroups)), "\\begin{{longtable}}{{{0:s}}}".format('l'*len(agroups)),
("\\caption{{Ligand interaction parameters. For interactions not " ("\\caption{{Ligand interaction parameters. For interactions not "
"listed, the default value of {0:s} is applied.}}").format( "listed, the default value of {0:s} is applied.}}").format(
str(self.sidechain_cutoffs.default)), str(self.sidechain_cutoffs.default)),

View File

@@ -1,7 +1,9 @@
"""Entry point for PROPKA script.""" """Entry point for PROPKA script."""
import logging import logging
from propka.lib import loadOptions from propka.lib import loadOptions
from propka.molecular_container import Molecular_container from propka.input import read_parameter_file, read_molecule_file
from propka.parameters import Parameters
from propka.molecular_container import MolecularContainer
_LOGGER = logging.getLogger("PROPKA") _LOGGER = logging.getLogger("PROPKA")
@@ -13,8 +15,10 @@ def main(optargs=None):
optargs = optargs if optargs is not None else [] optargs = optargs if optargs is not None else []
options = loadOptions(*optargs) options = loadOptions(*optargs)
pdbfiles = options.filenames pdbfiles = options.filenames
parameters = read_parameter_file(options.parameters, Parameters())
for pdbfile in pdbfiles: for pdbfile in pdbfiles:
my_molecule = Molecular_container(pdbfile, options) my_molecule = MolecularContainer(parameters, options)
my_molecule = read_molecule_file(pdbfile, my_molecule)
my_molecule.calculate_pka() my_molecule.calculate_pka()
my_molecule.write_pka() my_molecule.write_pka()
@@ -33,9 +37,11 @@ def single(pdbfile, optargs=None):
optargs = optargs if optargs is not None else [] optargs = optargs if optargs is not None else []
options = loadOptions(*optargs) options = loadOptions(*optargs)
pdbfile = options.filenames.pop(0) pdbfile = options.filenames.pop(0)
parameters = read_parameter_file(options.parameters, Parameters())
if len(options.filenames) > 0: if len(options.filenames) > 0:
_LOGGER.warning("Ignoring filenames: {0:s}".format(options.filenames)) _LOGGER.warning("Ignoring filenames: {0:s}".format(options.filenames))
my_molecule = Molecular_container(pdbfile, options) my_molecule = MolecularContainer(parameters, options)
my_molecule = read_molecule_file(pdbfile, my_molecule)
my_molecule.calculate_pka() my_molecule.calculate_pka()
my_molecule.write_pka() my_molecule.write_pka()
return my_molecule return my_molecule

View File

@@ -3,7 +3,13 @@
TODO - this module unnecessarily confuses the code. Can we eliminate it? TODO - this module unnecessarily confuses the code. Can we eliminate it?
""" """
from propka.lib import info from propka.lib import info
import propka.calculations as calcs from propka.hydrogens import setup_bonding_and_protonation, setup_bonding
from propka.hydrogens import setup_bonding_and_protonation_30_style
from propka.energy import radial_volume_desolvation, calculate_pair_weight
from propka.energy import hydrogen_bond_energy, hydrogen_bond_interaction
from propka.energy import electrostatic_interaction, check_coulomb_pair
from propka.energy import coulomb_energy, check_exceptions
from propka.energy import backbone_reorganization
class Version: class Version:
@@ -79,8 +85,7 @@ class Version:
def setup_bonding_and_protonation(self, molecular_container): def setup_bonding_and_protonation(self, molecular_container):
"""Setup bonding and protonation using assigned model.""" """Setup bonding and protonation using assigned model."""
return self.molecular_preparation_method( return self.molecular_preparation_method(molecular_container)
self.parameters, molecular_container)
def setup_bonding(self, molecular_container): def setup_bonding(self, molecular_container):
"""Setup bonding using assigned model.""" """Setup bonding using assigned model."""
@@ -94,18 +99,18 @@ class VersionA(Version):
"""Initialize object with parameters.""" """Initialize object with parameters."""
# set the calculation rutines used in this version # set the calculation rutines used in this version
super().__init__(parameters) super().__init__(parameters)
self.molecular_preparation_method = calcs.setup_bonding_and_protonation self.molecular_preparation_method = setup_bonding_and_protonation
self.prepare_bonds = calcs.setup_bonding self.prepare_bonds = setup_bonding
self.desolvation_model = calcs.radial_volume_desolvation self.desolvation_model = radial_volume_desolvation
self.weight_pair_method = calcs.calculate_pair_weight self.weight_pair_method = calculate_pair_weight
self.sidechain_interaction_model = calcs.hydrogen_bond_energy self.sidechain_interaction_model = hydrogen_bond_energy
self.hydrogen_bond_interaction_model = calcs.hydrogen_bond_interaction self.hydrogen_bond_interaction_model = hydrogen_bond_interaction
self.electrostatic_interaction_model = calcs.electrostatic_interaction self.electrostatic_interaction_model = electrostatic_interaction
self.check_coulomb_pair_method = calcs.check_coulomb_pair self.check_coulomb_pair_method = check_coulomb_pair
self.coulomb_interaction_model = calcs.coulomb_energy self.coulomb_interaction_model = coulomb_energy
self.backbone_interaction_model = calcs.hydrogen_bond_energy self.backbone_interaction_model = hydrogen_bond_energy
self.backbone_reorganisation_method = calcs.backbone_reorganization self.backbone_reorganisation_method = backbone_reorganization
self.exception_check_method = calcs.check_exceptions self.exception_check_method = check_exceptions
def get_hydrogen_bond_parameters(self, atom1, atom2): def get_hydrogen_bond_parameters(self, atom1, atom2):
"""Get hydrogen bond parameters for two atoms. """Get hydrogen bond parameters for two atoms.
@@ -265,14 +270,14 @@ class Propka30(Version):
# set the calculation routines used in this version # set the calculation routines used in this version
super().__init__(parameters) super().__init__(parameters)
self.molecular_preparation_method = ( self.molecular_preparation_method = (
calcs.setup_bonding_and_protonation_30_style) setup_bonding_and_protonation_30_style)
self.desolvation_model = calcs.radial_volume_desolvation self.desolvation_model = radial_volume_desolvation
self.weight_pair_method = calcs.calculate_pair_weight self.weight_pair_method = calculate_pair_weight
self.sidechain_interaction_model = calcs.hydrogen_bond_energy self.sidechain_interaction_model = hydrogen_bond_energy
self.check_coulomb_pair_method = calcs.check_coulomb_pair self.check_coulomb_pair_method = check_coulomb_pair
self.coulomb_interaction_model = calcs.coulomb_energy self.coulomb_interaction_model = coulomb_energy
self.backbone_reorganisation_method = calcs.backbone_reorganization self.backbone_reorganisation_method = backbone_reorganization
self.exception_check_method = calcs.check_exceptions self.exception_check_method = check_exceptions
def get_hydrogen_bond_parameters(self, atom1, atom2): def get_hydrogen_bond_parameters(self, atom1, atom2):
"""Get hydrogen bond parameters for two atoms. """Get hydrogen bond parameters for two atoms.

View File

@@ -11,7 +11,9 @@ is the same as the module name; that's why the new script is called
propka31.) propka31.)
""" """
from propka.lib import loadOptions from propka.lib import loadOptions
from propka.molecular_container import Molecular_container from propka.input import read_parameter_file, read_molecule_file
from propka.parameters import Parameters
from propka.molecular_container import MolecularContainer
def main(): def main():
@@ -19,9 +21,11 @@ def main():
# loading options, flaggs and arguments # loading options, flaggs and arguments
options = loadOptions([]) options = loadOptions([])
pdbfiles = options.filenames pdbfiles = options.filenames
parameters = read_parameter_file(options.parameters, Parameters())
for pdbfile in pdbfiles: for pdbfile in pdbfiles:
my_molecule = Molecular_container(pdbfile, options) my_molecule = MolecularContainer(parameters, options)
my_molecule = read_molecule_file(pdbfile, my_molecule)
my_molecule.calculate_pka() my_molecule.calculate_pka()
my_molecule.write_pka() my_molecule.write_pka()

View File

@@ -5,8 +5,10 @@ import re
from pathlib import Path from pathlib import Path
import pytest import pytest
from numpy.testing import assert_almost_equal from numpy.testing import assert_almost_equal
import propka.lib from propka.parameters import Parameters
import propka.molecular_container from propka.molecular_container import MolecularContainer
from propka.input import read_parameter_file, read_molecule_file
from propka.lib import loadOptions
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -64,15 +66,16 @@ def run_propka(options, pdb_path, tmp_path):
tmp_path: path for working directory tmp_path: path for working directory
""" """
options += [str(pdb_path)] options += [str(pdb_path)]
args = propka.lib.loadOptions(options) args = loadOptions(options)
try: try:
_LOGGER.warning( _LOGGER.warning(
"Working in tmpdir {0:s} because of PROPKA file output; " "Working in tmpdir {0:s} because of PROPKA file output; "
"need to fix this.".format(str(tmp_path))) "need to fix this.".format(str(tmp_path)))
cwd = Path.cwd() cwd = Path.cwd()
os.chdir(tmp_path) os.chdir(tmp_path)
molecule = propka.molecular_container.Molecular_container( parameters = read_parameter_file(args.parameters, Parameters())
str(pdb_path), args) molecule = MolecularContainer(parameters, args)
molecule = read_molecule_file(str(pdb_path), molecule)
molecule.calculate_pka() molecule.calculate_pka()
molecule.write_pka() molecule.write_pka()
finally: finally: