Source code for propka.molecular_container

"""
PDB molecular container
=======================

Molecular container for storing all contents of PDB files.
"""
import logging
import os
import propka.version
from propka.output import write_pka, print_header, print_result
from propka.conformation_container import ConformationContainer
from propka.lib import make_grid


_LOGGER = logging.getLogger(__name__)


# TODO - these are constants whose origins are a little murky
UNK_PI_CUTOFF = 0.01
# Maximum number of iterations for finding PI
MAX_ITERATION = 4


[docs]class MolecularContainer: """Container for storing molecular contents of PDB files. TODO - this class name does not conform to PEP8 but has external use. We should deprecate and change eventually. .. versionchanged:: 3.4.0 Removed :meth:`write_propka` and :meth:`additional_setup_when_reading_input_file` as reading and writing PROPKA input files is no longer supported. """ def __init__(self, parameters, options=None): """Initialize molecular container. Args: parameters: Parameters() object options: options object """ # printing out header before parsing input print_header() self.conformation_names = [] self.conformations = {} self.options = options self.name = None try: version_class = getattr(propka.version, parameters.version) self.version = version_class(parameters) except AttributeError as err: print(err) errstr = 'Error: Version {0:s} does not exist'.format( parameters.version) raise Exception(errstr)
[docs] def top_up_conformations(self): """Makes sure that all atoms are present in all conformations.""" first = self.conformations[self.conformation_names[0]] for name in self.conformation_names[1:]: if len(self.conformations[name]) < len(first): self.conformations[name].top_up(first)
[docs] def find_covalently_coupled_groups(self): """Find covalently coupled groups.""" for name in self.conformation_names: self.conformations[name].find_covalently_coupled_groups()
[docs] def find_non_covalently_coupled_groups(self): """Find non-covalently coupled groups.""" verbose = self.options.display_coupled_residues for name in self.conformation_names: self.conformations[name].find_non_covalently_coupled_groups( verbose=verbose)
[docs] def extract_groups(self): """Identify the groups needed for pKa calculation.""" for name in self.conformation_names: self.conformations[name].extract_groups()
[docs] def calculate_pka(self): """Calculate pKa values.""" # calculate for each conformation for name in self.conformation_names: self.conformations[name].calculate_pka( self.version, self.options) # find non-covalently coupled groups self.find_non_covalently_coupled_groups() # find the average of the conformations self.average_of_conformations() # print out the conformation-average results print_result(self, 'AVR', self.version.parameters)
[docs] def average_of_conformations(self): """Generate an average of conformations.""" parameters = self.conformations[self.conformation_names[0]].parameters # make a new configuration to hold the average values avr_conformation = ConformationContainer( name='average', parameters=parameters, molecular_container=self) container = self.conformations[self.conformation_names[0]] for group in container.get_groups_for_calculations(): # new group to hold average values avr_group = group.clone() # sum up all groups ... for name in self.conformation_names: group_to_add = self.conformations[name].find_group(group) if group_to_add: avr_group += group_to_add else: str_ = ( 'Group {0:s} could not be found in ' 'conformation {1:s}.'.format( group.atom.residue_label, name)) _LOGGER.warning(str_) # ... and store the average value avr_group = avr_group / len(self.conformation_names) avr_conformation.groups.append(avr_group) # store information on coupling in the average container if len(list(filter(lambda c: c.non_covalently_coupled_groups, self.conformations.values()))): avr_conformation.non_covalently_coupled_groups = True # store chain info avr_conformation.chains = self.conformations[ self.conformation_names[0]].chains self.conformations['AVR'] = avr_conformation
[docs] def write_pka(self, filename=None, reference="neutral", direction="folding", options=None): """Write pKa information to a file. Args: filename: file to write to reference: reference state direction: folding vs. unfolding options: options object """ if filename is None: filename = os.path.join('{0:s}.pka'.format(self.name)) # if the display_coupled_residues option is true, write the results out # to an alternative pka file if self.options.display_coupled_residues: filename = os.path.join('{0:s}_alt_state.pka'.format(self.name)) if (hasattr(self.version.parameters, 'output_file_tag') and len(self.version.parameters.output_file_tag) > 0): filename = os.path.join( '{0:s}_{1:s}.pka'.format( self.name, self.version.parameters.output_file_tag)) write_pka( self, self.version.parameters, filename=filename, conformation='AVR', reference=reference)
[docs] def get_folding_profile(self, conformation='AVR', reference="neutral", grid=[0., 14., 0.1]): """Get a folding profile. Args: conformation: conformation to select reference: reference state direction: folding direction (folding) grid: the grid of pH values [min, max, step_size] options: options object Returns: TODO - figure out what these are 1. profile 2. opt 3. range_80pct 4. stability_range """ # calculate stability profile profile = [] for ph in make_grid(*grid): conf = self.conformations[conformation] ddg = conf.calculate_folding_energy(ph=ph, reference=reference) profile.append([ph, ddg]) # find optimum opt = [None, 1e6] for point in profile: opt = min(opt, point, key=lambda v: v[1]) # find values within 80 % of optimum range_80pct = [None, None] values_within_80pct = [p[0] for p in profile if p[1] < 0.8*opt[1]] if len(values_within_80pct) > 0: range_80pct = [min(values_within_80pct), max(values_within_80pct)] # find stability range stability_range = [None, None] stable_values = [p[0] for p in profile if p[1] < 0.0] if len(stable_values) > 0: stability_range = [min(stable_values), max(stable_values)] return profile, opt, range_80pct, stability_range
[docs] def get_charge_profile(self, conformation='AVR', grid=[0., 14., .1]): """Get charge profile for conformation as function of pH. Args: conformation: conformation to test grid: grid of pH values [min, max, step] Returns: list of charge state values """ charge_profile = [] for ph in make_grid(*grid): conf = self.conformations[conformation] q_unfolded, q_folded = conf.calculate_charge( self.version.parameters, ph=ph) charge_profile.append([ph, q_unfolded, q_folded]) return charge_profile
[docs] def get_pi(self, conformation='AVR', grid=[0., 14., 1], iteration=0): """Get the isoelectric points for folded and unfolded states. Args: conformation: conformation to test grid: grid of pH values [min, max, step] iteration: iteration number of process Returns: 1. Folded state PI 2. Unfolded state PI """ charge_profile = self.get_charge_profile( conformation=conformation, grid=grid) pi_folded = pi_unfolded = [None, 1e6, 1e6] for point in charge_profile: pi_folded = min(pi_folded, point, key=lambda v: abs(v[2])) pi_unfolded = min(pi_unfolded, point, key=lambda v: abs(v[1])) # If results are not good enough, do it again with a higher sampling # resolution pi_folded_value = pi_folded[0] pi_unfolded_value = pi_unfolded[0] step = grid[2] # TODO - need to warn if maximum number of iterations is exceeded if ((pi_folded[2] > UNK_PI_CUTOFF or pi_unfolded[1] > UNK_PI_CUTOFF) and iteration < MAX_ITERATION): pi_folded_value, _ = self.get_pi( conformation=conformation, grid=[pi_folded[0]-step, pi_folded[0]+step, step/10.0], iteration=iteration+1) _, pi_unfolded_value = self.get_pi( conformation=conformation, grid=[pi_unfolded[0]-step, pi_unfolded[0]+step, step/10.0], iteration=iteration+1) return pi_folded_value, pi_unfolded_value