From 397d5e10aa02595b6f11a61ed762a1c94e7c8f59 Mon Sep 17 00:00:00 2001 From: Nathan Baker Date: Sat, 30 May 2020 09:26:13 -0700 Subject: [PATCH] Remove cyclic import based on atom.py Partially addresses https://github.com/jensengroup/propka-3.1/issues/49 --- propka/atom.py | 32 ++++----- propka/calculations.py | 3 - propka/group.py | 13 ++++ propka/ligand_pka_values.py | 7 +- propka/molecular_container.py | 16 ++--- propka/output.py | 121 ++++++++++++++++++++++++++++++++-- propka/pdb.py | 116 +------------------------------- 7 files changed, 155 insertions(+), 153 deletions(-) diff --git a/propka/atom.py b/propka/atom.py index 1e87af2..3bb3f12 100644 --- a/propka/atom.py +++ b/propka/atom.py @@ -1,8 +1,7 @@ """Atom class - contains all atom information found in the PDB file""" import string -import propka.lib -import propka.group from . import hybrid36 +from propka.lib import make_tidy_atom_label # Format strings that get used in multiple places (or are very complex) @@ -50,6 +49,9 @@ def __init__(self, line=None): self.z = None self.group = 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.cysteine_bridge = False self.bonded_atoms = [] @@ -267,7 +269,7 @@ def make_input_line(self): model_pka = PKA_FMT.format(self.group.model_pka) str_ = INPUT_LINE_FMT.format( 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) return str_ @@ -313,21 +315,11 @@ def get_input_parameters(self): self.occ = self.occ.replace('ALG', 'titratable_ligand') self.occ = self.occ.replace('BLG', 'titratable_ligand') self.occ = self.occ.replace('LG', 'non_titratable_ligand') - # try to initialise the group - 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_) + self.group_label = "{0:s}_group".format(self.occ) # set the model pKa value if self.beta != '-': - self.group.model_pka = float(self.beta) - self.group.model_pka_set = True + self.group_model_pka = float(self.beta) + self.group_model_pka_set = True # set occ and beta to standard values self.occ = '1.00' self.beta = '0.00' @@ -344,7 +336,7 @@ def make_pdb_line(self): """ str_ = PDB_LINE_FMT1.format( 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_ def make_mol2_line(self, id_): @@ -359,7 +351,7 @@ def make_mol2_line(self, id_): """ str_ = MOL2_LINE_FMT.format( 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_ def make_pdb_line2(self, numb=None, name=None, res_name=None, chain_id=None, @@ -397,7 +389,7 @@ def make_pdb_line2(self, numb=None, name=None, res_name=None, chain_id=None, str_ = PDB_LINE_FMT2.format( numb=numb, res_name=res_name, chain_id=chain_id, res_num=res_num, 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_ @@ -408,7 +400,7 @@ def get_tidy_label(self): Returns: 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): """Return an undefined-format string version of this atom.""" diff --git a/propka/calculations.py b/propka/calculations.py index df8db5d..915b713 100644 --- a/propka/calculations.py +++ b/propka/calculations.py @@ -1,8 +1,5 @@ """PROPKA calculations.""" import math -import propka.protonate -import propka.bonds -from propka.lib import warning, info # TODO - this file should be broken into three separate files: diff --git a/propka/group.py b/propka/group.py index 465c195..96c0a30 100644 --- a/propka/group.py +++ b/propka/group.py @@ -6,6 +6,7 @@ from propka.determinant import Determinant from propka.lib import info, warning + # Constants that start with "UNK_" are a mystery to me UNK_PKA_SCALING = -1.36 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(): return IonGroup(atom) 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 diff --git a/propka/ligand_pka_values.py b/propka/ligand_pka_values.py index d7a5423..abfc3e4 100644 --- a/propka/ligand_pka_values.py +++ b/propka/ligand_pka_values.py @@ -6,7 +6,7 @@ import propka.calculations import propka.parameters import propka.pdb -import propka.lib +from propka.output import write_mol2_for_atoms from propka.lib import info, warning @@ -133,7 +133,7 @@ def get_marvin_pkas_for_molecule(self, atoms, filename='__tmp_ligand.mol2', """ # print out structure unless we are using user-modified structure 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 if not os.path.isfile(filename): errstr = ( @@ -141,7 +141,7 @@ def get_marvin_pkas_for_molecule(self, atoms, filename='__tmp_ligand.mol2', "- generating one".format( filename)) warning(errstr) - propka.pdb.write_mol2_for_atoms(atoms, filename) + write_mol2_for_atoms(atoms, filename) # Marvin calculate pKa values fmt = ( 'pka -a {num1} -b {num2} --min {min_ph} ' @@ -197,3 +197,4 @@ def extract_pkas(output): if len(indices) != len(values) != len(types): raise Exception('Lengths of atoms and pka values mismatch') return indices, values, types + diff --git a/propka/molecular_container.py b/propka/molecular_container.py index 3d2409f..d99c2c0 100644 --- a/propka/molecular_container.py +++ b/propka/molecular_container.py @@ -1,13 +1,11 @@ """Molecular container for storing all contents of PDB files.""" import os import sys -import propka.pdb import propka.version -import propka.output -import propka.group -import propka.lib +from propka.pdb import read_input +from propka.output import write_input 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 @@ -78,7 +76,7 @@ def __init__(self, input_file, options=None): 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) + write_input(self, filename) elif input_file_extension == '.propka_input': #input is a propka_input file [self.conformations, self.conformation_names] = ( @@ -155,7 +153,7 @@ def average_of_conformations(self): else: str_ = ( 'Group {0:s} could not be found in ' - 'conformation {0:s}.'.format( + 'conformation {1:s}.'.format( group.atom.residue_label, name)) warning(str_) # ... and store the average value @@ -214,7 +212,7 @@ def get_folding_profile(self, conformation='AVR', reference="neutral", """ # calculate stability profile profile = [] - for ph in propka.lib.make_grid(*grid): + for ph in make_grid(*grid): conf = self.conformations[conformation] ddg = conf.calculate_folding_energy(ph=ph, reference=reference) profile.append([ph, ddg]) @@ -244,7 +242,7 @@ def get_charge_profile(self, conformation='AVR', grid=[0., 14., .1]): list of charge state values """ charge_profile = [] - for ph in propka.lib.make_grid(*grid): + for ph in make_grid(*grid): conf = self.conformations[conformation] q_unfolded, q_folded = conf.calculate_charge( self.version.parameters, ph=ph) diff --git a/propka/output.py b/propka/output.py index 61f001b..61f8a68 100644 --- a/propka/output.py +++ b/propka/output.py @@ -1,6 +1,6 @@ """Output routines.""" from datetime import date -from propka.lib import info +from propka.lib import info, open_file_for_writing def print_header(): @@ -11,8 +11,8 @@ def print_header(): info(str_) -def write_pdb(protein, pdbfile=None, filename=None, include_hydrogens=False, - _=None): +def write_pdb_for_protein( + protein, pdbfile=None, filename=None, include_hydrogens=False, _=None): """Write a residue to the new PDB file. Args: @@ -50,6 +50,16 @@ def write_pdb(protein, pdbfile=None, filename=None, include_hydrogens=False, 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', reference="neutral", _="folding", verbose=False, __=None): @@ -204,8 +214,8 @@ def get_summary_section(protein, conformation, parameters): def get_folding_profile_section( - protein, conformation='AVR', direction="folding", reference="neutral", - window=[0., 14., 1.0], _=False, __=None): + protein, conformation='AVR', direction="folding", reference="neutral", + window=[0., 14., 1.0], _=False, __=None): """Returns string with the folding profile section of the results. Args: @@ -461,3 +471,104 @@ def make_interaction_map(name, list_, interaction): tag = ' X ' res += '{0:>10s}| '.format(tag) 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 = '@MOLECULE\n\n{natom:d} {id:d}\nSMALL\nUSER_CHARGES\n' + atoms_section = '@ATOM\n' + for i, atom in enumerate(atoms): + atoms_section += atom.make_mol2_line(i+1) + bonds_section = '@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 = '@SUBSTRUCTURE\n\n' + if len(atoms) > 0: + substructure_section = ( + '@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_input(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() diff --git a/propka/pdb.py b/propka/pdb.py index 0074938..f1eaf16 100644 --- a/propka/pdb.py +++ b/propka/pdb.py @@ -1,7 +1,8 @@ -"""PDB parsing functionality.""" +"""Read and parse PDB-like input files.""" import propka.lib from propka.lib import warning from propka.atom import Atom +from propka.group import initialize_atom_group from propka.conformation_container import ConformationContainer @@ -158,118 +159,6 @@ def get_atom_lines_from_pdb(pdb_file, ignore_residues=[], keep_protons=False, terminal = None -def write_pdb(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_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 = '@MOLECULE\n\n{natom:d} {id:d}\nSMALL\nUSER_CHARGES\n' - atoms_section = '@ATOM\n' - for i, atom in enumerate(atoms): - atoms_section += atom.make_mol2_line(i+1) - bonds_section = '@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 = '@SUBSTRUCTURE\n\n' - if len(atoms) > 0: - substructure_section = ( - '@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. @@ -316,6 +205,7 @@ def get_atom_lines_from_input(input_file, tags=['ATOM ', 'HETATM']): if tag in tags: atom = Atom(line=line) atom.get_input_parameters() + initialize_atom_group(atom) atom.groups_extracted = 1 atom.is_protonated = True atoms[atom.numb] = atom