Typing: Parameters, vector_algebra

- Parameters typing
- vector_algebra tests and refactoring
- Various pyright typing improvements
This commit is contained in:
Thomas Holder
2023-12-12 20:41:17 +01:00
parent 4106726483
commit 57ad5d8384
18 changed files with 488 additions and 183 deletions

View File

@@ -7,6 +7,8 @@ Container data structure for molecular conformations.
import logging import logging
import functools import functools
from typing import Iterable, List, NoReturn, Optional, TYPE_CHECKING, Set from typing import Iterable, List, NoReturn, Optional, TYPE_CHECKING, Set
from propka.lib import Options
from propka.version import Version
if TYPE_CHECKING: if TYPE_CHECKING:
from propka.atom import Atom from propka.atom import Atom
@@ -19,6 +21,7 @@ from propka.coupled_groups import NCCG
from propka.determinants import set_backbone_determinants, set_ion_determinants from propka.determinants import set_backbone_determinants, set_ion_determinants
from propka.determinants import set_determinants from propka.determinants import set_determinants
from propka.group import Group, is_group from propka.group import Group, is_group
from propka.parameters import Parameters
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -44,9 +47,9 @@ class ConformationContainer:
""" """
def __init__(self, def __init__(self,
name: str = '', name: str,
parameters=None, parameters: Parameters,
molecular_container: Optional["MolecularContainer"] = None): molecular_container: "MolecularContainer"):
"""Initialize conformation container. """Initialize conformation container.
Args: Args:
@@ -145,6 +148,7 @@ class ConformationContainer:
Returns: Returns:
a set of bonded atom groups a set of bonded atom groups
""" """
assert self.parameters is not None
res: Set[Group] = set() res: Set[Group] = set()
for bond_atom in atom.bonded_atoms: for bond_atom in atom.bonded_atoms:
# skip the original atom # skip the original atom
@@ -187,7 +191,7 @@ class ConformationContainer:
# If --titrate_only option is set, make non-specified residues # If --titrate_only option is set, make non-specified residues
# un-titratable: # un-titratable:
assert self.molecular_container is not None assert self.molecular_container.options is not None
titrate_only = self.molecular_container.options.titrate_only titrate_only = self.molecular_container.options.titrate_only
if titrate_only is not None: if titrate_only is not None:
atom = group.atom atom = group.atom
@@ -196,7 +200,7 @@ class ConformationContainer:
if group.residue_type == 'CYS': if group.residue_type == 'CYS':
group.exclude_cys_from_results = True group.exclude_cys_from_results = True
def calculate_pka(self, version, options): def calculate_pka(self, version: Version, options: Options):
"""Calculate pKas for conformation container. """Calculate pKas for conformation container.
Args: Args:
@@ -349,7 +353,7 @@ class ConformationContainer:
reference=reference) reference=reference)
return ddg return ddg
def calculate_charge(self, parameters, ph=None): def calculate_charge(self, parameters, ph: float):
"""Calculate charge for folded and unfolded states. """Calculate charge for folded and unfolded states.
Args: Args:

View File

@@ -33,6 +33,7 @@ class NonCovalentlyCoupledGroups:
Returns: Returns:
dictionary describing coupling dictionary describing coupling
""" """
assert self.parameters is not None
# check if the interaction energy is high enough # check if the interaction energy is high enough
interaction_energy = max(self.get_interaction(group1, group2), interaction_energy = max(self.get_interaction(group1, group2),
self.get_interaction(group2, group1)) self.get_interaction(group2, group1))
@@ -105,6 +106,7 @@ class NonCovalentlyCoupledGroups:
Returns: Returns:
float value of scaling factor float value of scaling factor
""" """
assert self.parameters is not None
intrinsic_pka_diff = abs(pka1-pka2) intrinsic_pka_diff = abs(pka1-pka2)
res = 0.0 res = 0.0
if intrinsic_pka_diff <= self.parameters.max_intrinsic_pka_diff: if intrinsic_pka_diff <= self.parameters.max_intrinsic_pka_diff:
@@ -122,6 +124,7 @@ class NonCovalentlyCoupledGroups:
Returns: Returns:
float value of scaling factor float value of scaling factor
""" """
assert self.parameters is not None
free_energy_diff = abs(energy1-energy2) free_energy_diff = abs(energy1-energy2)
res = 0.0 res = 0.0
if free_energy_diff <= self.parameters.max_free_energy_diff: if free_energy_diff <= self.parameters.max_free_energy_diff:
@@ -136,6 +139,7 @@ class NonCovalentlyCoupledGroups:
Returns: Returns:
float value of scaling factor float value of scaling factor
""" """
assert self.parameters is not None
res = 0.0 res = 0.0
interaction_energy = abs(interaction_energy) interaction_energy = abs(interaction_energy)
if interaction_energy >= self.parameters.min_interaction_energy: if interaction_energy >= self.parameters.min_interaction_energy:

View File

@@ -14,12 +14,18 @@ Functions to manipulate :class:`propka.determinant.Determinant` objects.
""" """
import math import math
from typing import List
import propka.calculations
import propka.iterative 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.energy 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
from propka.group import Group
from propka.iterative import Interaction
from propka.version import Version
# Cutoff for angle factor # Cutoff for angle factor
@@ -28,7 +34,7 @@ from propka.determinant import Determinant
FANGLE_MIN = 0.001 FANGLE_MIN = 0.001
def set_determinants(propka_groups, version=None, options=None): def set_determinants(propka_groups: List[Group], version: Version, options=None):
"""Add side-chain and coulomb determinants/perturbations to all residues. """Add side-chain and coulomb determinants/perturbations to all residues.
NOTE - backbone determinants are set separately NOTE - backbone determinants are set separately
@@ -38,7 +44,7 @@ def set_determinants(propka_groups, version=None, options=None):
version: version object version: version object
options: options object options: options object
""" """
iterative_interactions = [] iterative_interactions: List[Interaction] = []
# --- NonIterative section ---# # --- NonIterative section ---#
for group1 in propka_groups: for group1 in propka_groups:
for group2 in propka_groups: for group2 in propka_groups:
@@ -77,7 +83,7 @@ def add_determinants(group1, group2, distance, version):
add_coulomb_determinants(group1, group2, distance, version) add_coulomb_determinants(group1, group2, distance, version)
def add_sidechain_determinants(group1, group2, version=None): def add_sidechain_determinants(group1: Group, group2: Group, version: Version):
"""Add side-chain determinants and perturbations. """Add side-chain determinants and perturbations.
NOTE - res_num1 > res_num2 NOTE - res_num1 > res_num2
@@ -236,6 +242,8 @@ def set_backbone_determinants(titratable_groups, backbone_groups, version):
get_smallest_distance( get_smallest_distance(
backbone_interaction_atoms, backbone_interaction_atoms,
titratable_group_interaction_atoms)) titratable_group_interaction_atoms))
assert backbone_atom is not None
assert titratable_atom is not None
# get the parameters # get the parameters
parameters = ( parameters = (
version.get_backbone_hydrogen_bond_parameters( version.get_backbone_hydrogen_bond_parameters(

View File

@@ -7,11 +7,15 @@ Energy calculations.
""" """
import math import math
import logging import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional, Sequence
from propka.atom import Atom
from propka.parameters import Parameters
if TYPE_CHECKING: if TYPE_CHECKING:
from propka.conformation_container import ConformationContainer from propka.conformation_container import ConformationContainer
from propka.group import Group from propka.group import Group
from propka.version import Version
from propka.calculations import squared_distance, get_smallest_distance from propka.calculations import squared_distance, get_smallest_distance
@@ -89,7 +93,7 @@ def calculate_scale_factor(parameters, weight: float) -> float:
return scale_factor return scale_factor
def calculate_weight(parameters, num_volume: int) -> float: def calculate_weight(parameters: Parameters, num_volume: float) -> float:
"""Calculate the atom-based desolvation weight. """Calculate the atom-based desolvation weight.
TODO - figure out why a similar function exists in version.py TODO - figure out why a similar function exists in version.py
@@ -109,7 +113,7 @@ def calculate_weight(parameters, num_volume: int) -> float:
return weight return weight
def calculate_pair_weight(parameters, num_volume1: int, num_volume2: int) -> float: def calculate_pair_weight(parameters: Parameters, num_volume1: int, num_volume2: int) -> float:
"""Calculate the atom-pair based desolvation weight. """Calculate the atom-pair based desolvation weight.
Args: Args:
@@ -148,7 +152,11 @@ def hydrogen_bond_energy(dist, dpka_max: float, cutoffs, f_angle=1.0) -> float:
return abs(dpka) return abs(dpka)
def angle_distance_factors(atom1=None, atom2=None, atom3=None, center=None): def angle_distance_factors(
atom1: Optional[Atom] = None,
atom2: Atom = None, # type: ignore[assignment]
atom3: Atom = None, # type: ignore[assignment]
center: Optional[Sequence[float]] = None):
"""Calculate distance and angle factors for three atoms for backbone """Calculate distance and angle factors for three atoms for backbone
interactions. interactions.
@@ -182,6 +190,7 @@ def angle_distance_factors(atom1=None, atom2=None, atom3=None, center=None):
dy_32 = dy_32/dist_23 dy_32 = dy_32/dist_23
dz_32 = dz_32/dist_23 dz_32 = dz_32/dist_23
if atom1 is None: if atom1 is None:
assert center is not None
dx_21 = center[0] - atom2.x dx_21 = center[0] - atom2.x
dy_21 = center[1] - atom2.y dy_21 = center[1] - atom2.y
dz_21 = center[2] - atom2.z dz_21 = center[2] - atom2.z
@@ -197,7 +206,7 @@ def angle_distance_factors(atom1=None, atom2=None, atom3=None, center=None):
return dist_12, f_angle, dist_23 return dist_12, f_angle, dist_23
def hydrogen_bond_interaction(group1, group2, version): def hydrogen_bond_interaction(group1: "Group", group2: "Group", version: "Version"):
"""Calculate energy for hydrogen bond interactions between two groups. """Calculate energy for hydrogen bond interactions between two groups.
Args: Args:
@@ -213,7 +222,7 @@ def hydrogen_bond_interaction(group1, group2, version):
[closest_atom1, dist, closest_atom2] = get_smallest_distance( [closest_atom1, dist, closest_atom2] = get_smallest_distance(
atoms1, atoms2 atoms1, atoms2
) )
if None in [closest_atom1, closest_atom2]: if closest_atom1 is None or closest_atom2 is None:
_LOGGER.warning( _LOGGER.warning(
'Side chain interaction failed for {0:s} and {1:s}'.format( 'Side chain interaction failed for {0:s} and {1:s}'.format(
group1.label, group2.label)) group1.label, group2.label))
@@ -297,7 +306,7 @@ def electrostatic_interaction(group1, group2, dist, version):
return value return value
def check_coulomb_pair(parameters, group1, group2, dist): def check_coulomb_pair(parameters: Parameters, group1: "Group", group2: "Group", dist: float) -> bool:
"""Checks if this Coulomb interaction should be done. """Checks if this Coulomb interaction should be done.
NOTE - this is a propka2.0 hack NOTE - this is a propka2.0 hack

View File

@@ -14,6 +14,7 @@ import math
from typing import cast, Dict, Iterable, List, NoReturn, Optional from typing import cast, Dict, Iterable, List, NoReturn, Optional
import propka.ligand import propka.ligand
from propka.parameters import Parameters
import propka.protonate import propka.protonate
from propka.atom import Atom from propka.atom import Atom
from propka.ligand_pka_values import LigandPkaValues from propka.ligand_pka_values import LigandPkaValues
@@ -90,7 +91,7 @@ class Group:
self.y = 0.0 self.y = 0.0
self.z = 0.0 self.z = 0.0
self.charge = 0 self.charge = 0
self.parameters = None self.parameters: Optional[Parameters] = None
self.exclude_cys_from_results = False self.exclude_cys_from_results = False
self.interaction_atoms_for_acids: List[Atom] = [] self.interaction_atoms_for_acids: List[Atom] = []
self.interaction_atoms_for_bases: List[Atom] = [] self.interaction_atoms_for_bases: List[Atom] = []
@@ -320,6 +321,7 @@ class Group:
def setup(self): def setup(self):
"""Set up a group.""" """Set up a group."""
assert self.parameters is not None
# set the charges # set the charges
if self.type in self.parameters.charge.keys(): if self.type in self.parameters.charge.keys():
self.charge = self.parameters.charge[self.type] self.charge = self.parameters.charge[self.type]
@@ -403,7 +405,7 @@ class Group:
' {0:s}'.format( ' {0:s}'.format(
str(self.interaction_atoms_for_bases[i]))) str(self.interaction_atoms_for_bases[i])))
def get_interaction_atoms(self, interacting_group) -> List[Atom]: def get_interaction_atoms(self, interacting_group: "Group") -> List[Atom]:
"""Get atoms involved in interaction with other group. """Get atoms involved in interaction with other group.
Args: Args:
@@ -592,7 +594,7 @@ class Group:
ddg = ddg_neutral + ddg_low ddg = ddg_neutral + ddg_low
return ddg return ddg
def calculate_charge(self, _, ph=7.0, state='folded'): def calculate_charge(self, _, ph: float = 7.0, state: str = 'folded') -> float:
"""Calculate the charge of the specified state at the specified pH. """Calculate the charge of the specified state at the specified pH.
Args: Args:
@@ -610,7 +612,7 @@ class Group:
charge = self.charge*(conc_ratio/(1.0+conc_ratio)) charge = self.charge*(conc_ratio/(1.0+conc_ratio))
return charge return charge
def use_in_calculations(self): def use_in_calculations(self) -> bool:
"""Indicate whether group should be included in results report. """Indicate whether group should be included in results report.
If --titrate_only option is specified, only residues that are If --titrate_only option is specified, only residues that are
@@ -1219,7 +1221,7 @@ class TitratableLigandGroup(Group):
self.model_pka_set = True self.model_pka_set = True
def is_group(parameters, atom: Atom) -> Optional[Group]: def is_group(parameters: Parameters, atom: Atom) -> Optional[Group]:
"""Identify whether the atom belongs to a group. """Identify whether the atom belongs to a group.
Args: Args:
@@ -1246,7 +1248,7 @@ def is_group(parameters, atom: Atom) -> Optional[Group]:
ligand_group = is_ligand_group_by_groups(parameters, atom) ligand_group = is_ligand_group_by_groups(parameters, atom)
else: else:
raise Exception( raise Exception(
'Unknown ligand typing method \'{0.s}\''.format( 'Unknown ligand typing method \'{0:s}\''.format(
parameters.ligand_typing)) parameters.ligand_typing))
if ligand_group: if ligand_group:
return ligand_group return ligand_group
@@ -1369,7 +1371,7 @@ def is_ligand_group_by_groups(_, atom: Atom) -> Optional[Group]:
return None return None
def is_ligand_group_by_marvin_pkas(parameters, atom: Atom) -> Optional[Group]: def is_ligand_group_by_marvin_pkas(parameters: Parameters, atom: Atom) -> Optional[Group]:
"""Identify whether the atom belongs to a ligand group by calculating """Identify whether the atom belongs to a ligand group by calculating
'Marvin pKas'. 'Marvin pKas'.

View File

@@ -122,6 +122,12 @@ def add_arg_hydrogen(residue: List[Atom]) -> List[Atom]:
Returns: Returns:
list of hydrogen atoms list of hydrogen atoms
""" """
cd_atom: Optional[Atom] = None
cz_atom: Optional[Atom] = None
ne_atom: Optional[Atom] = None
nh1_atom: Optional[Atom] = None
nh2_atom: Optional[Atom] = None
for atom in residue: for atom in residue:
if atom.name == "CD": if atom.name == "CD":
cd_atom = atom cd_atom = atom
@@ -133,6 +139,11 @@ def add_arg_hydrogen(residue: List[Atom]) -> List[Atom]:
nh1_atom = atom nh1_atom = atom
elif atom.name == "NH2": elif atom.name == "NH2":
nh2_atom = atom nh2_atom = atom
if (cd_atom is None or cz_atom is None or ne_atom is None or nh1_atom is None
or nh2_atom is None):
raise ValueError("Unable to find all atoms")
h1_atom = protonate_sp2(cd_atom, ne_atom, cz_atom) h1_atom = protonate_sp2(cd_atom, ne_atom, cz_atom)
h1_atom.name = "HE" h1_atom.name = "HE"
h2_atom = protonate_direction(nh1_atom, ne_atom, cz_atom) h2_atom = protonate_direction(nh1_atom, ne_atom, cz_atom)
@@ -152,6 +163,12 @@ def add_his_hydrogen(residue: List[Atom]) -> None:
Args: Args:
residue: histidine residue to protonate residue: histidine residue to protonate
""" """
cg_atom: Optional[Atom] = None
nd_atom: Optional[Atom] = None
cd_atom: Optional[Atom] = None
ce_atom: Optional[Atom] = None
ne_atom: Optional[Atom] = None
for atom in residue: for atom in residue:
if atom.name == "CG": if atom.name == "CG":
cg_atom = atom cg_atom = atom
@@ -163,6 +180,11 @@ def add_his_hydrogen(residue: List[Atom]) -> None:
ce_atom = atom ce_atom = atom
elif atom.name == "NE2": elif atom.name == "NE2":
ne_atom = atom ne_atom = atom
if (cg_atom is None or nd_atom is None or cd_atom is None or ce_atom is None
or ne_atom is None):
raise ValueError("Unable to find all atoms")
hd_atom = protonate_sp2(cg_atom, nd_atom, ce_atom) hd_atom = protonate_sp2(cg_atom, nd_atom, ce_atom)
hd_atom.name = "HND" hd_atom.name = "HND"
he_atom = protonate_sp2(cd_atom, ne_atom, ce_atom) he_atom = protonate_sp2(cd_atom, ne_atom, ce_atom)
@@ -177,6 +199,7 @@ def add_trp_hydrogen(residue: List[Atom]) -> None:
""" """
cd_atom = None cd_atom = None
ne_atom = None ne_atom = None
ce_atom = None
for atom in residue: for atom in residue:
if atom.name == "CD1": if atom.name == "CD1":
cd_atom = atom cd_atom = atom

View File

@@ -7,7 +7,10 @@ involve :class:`propka.determinant.Determinant` instances.
""" """
import logging import logging
from typing import Dict, Iterable, List, Optional, Sequence, Tuple
from propka.determinant import Determinant from propka.determinant import Determinant
from propka.group import Group
from propka.version import Version
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -16,9 +19,11 @@ _LOGGER = logging.getLogger(__name__)
# TODO - these are undocumented constants # TODO - these are undocumented constants
UNK_MIN_VALUE = 0.005 UNK_MIN_VALUE = 0.005
Interaction = list
def add_to_determinant_list(group1, group2, distance, iterative_interactions,
version): def add_to_determinant_list(group1: Group, group2: Group, distance: float,
iterative_interactions: List[Interaction], version: Version):
"""Add iterative determinantes to the list. """Add iterative determinantes to the list.
[[R1, R2], [side-chain, coulomb], [A1, A2]], ... [[R1, R2], [side-chain, coulomb], [A1, A2]], ...
@@ -48,7 +53,8 @@ def add_to_determinant_list(group1, group2, distance, iterative_interactions,
iterative_interactions.append(interaction) iterative_interactions.append(interaction)
def add_iterative_acid_pair(object1, object2, interaction): def add_iterative_acid_pair(object1: "Iterative", object2: "Iterative",
interaction: Interaction):
"""Add the Coulomb 'iterative' interaction (an acid pair). """Add the Coulomb 'iterative' interaction (an acid pair).
The higher pKa is raised with QQ+HB The higher pKa is raised with QQ+HB
@@ -90,7 +96,8 @@ def add_iterative_acid_pair(object1, object2, interaction):
annihilation[1] = -diff annihilation[1] = -diff
def add_iterative_base_pair(object1, object2, interaction): def add_iterative_base_pair(object1: "Iterative", object2: "Iterative",
interaction: Interaction):
"""Add the Coulomb 'iterative' interaction (a base pair). """Add the Coulomb 'iterative' interaction (a base pair).
The lower pKa is lowered The lower pKa is lowered
@@ -132,7 +139,8 @@ def add_iterative_base_pair(object1, object2, interaction):
annihilation[1] = -diff annihilation[1] = -diff
def add_iterative_ion_pair(object1, object2, interaction, version): def add_iterative_ion_pair(object1: "Iterative", object2: "Iterative",
interaction: Interaction, version: Version):
"""Add the Coulomb 'iterative' interaction (an acid-base pair) """Add the Coulomb 'iterative' interaction (an acid-base pair)
the pKa of the acid is lowered & the pKa of the base is raised the pKa of the acid is lowered & the pKa of the base is raised
@@ -194,7 +202,7 @@ def add_iterative_ion_pair(object1, object2, interaction, version):
object2.determinants['sidechain'].append(interaction) object2.determinants['sidechain'].append(interaction)
def add_determinants(iterative_interactions, version, _=None): def add_determinants(iterative_interactions: List[Interaction], version: Version, _=None):
"""Add determinants iteratively. """Add determinants iteratively.
The iterative pKa scheme. Later it is all added in 'calculateTotalPKA' The iterative pKa scheme. Later it is all added in 'calculateTotalPKA'
@@ -205,7 +213,7 @@ def add_determinants(iterative_interactions, version, _=None):
_: options object _: options object
""" """
# --- setup --- # --- setup ---
iteratives = [] iteratives: List[Iterative] = []
done_group = [] done_group = []
# create iterative objects with references to their real group counterparts # create iterative objects with references to their real group counterparts
for interaction in iterative_interactions: for interaction in iterative_interactions:
@@ -270,6 +278,7 @@ def add_determinants(iterative_interactions, version, _=None):
# reset pka_old & storing pka_new in pka_iter # reset pka_old & storing pka_new in pka_iter
for itres in iteratives: for itres in iteratives:
assert itres.pka_new is not None
itres.pka_old = itres.pka_new itres.pka_old = itres.pka_new
itres.pka_iter.append(itres.pka_new) itres.pka_iter.append(itres.pka_new)
@@ -302,7 +311,10 @@ def add_determinants(iterative_interactions, version, _=None):
itres.group.determinants[type_].append(new_det) itres.group.determinants[type_].append(new_det)
def find_iterative(pair, iteratives): def find_iterative(
pair: Sequence[Group],
iteratives: Iterable["Iterative"],
) -> Tuple["Iterative", "Iterative"]:
"""Find the 'iteratives' that correspond to the groups in 'pair'. """Find the 'iteratives' that correspond to the groups in 'pair'.
Args: Args:
@@ -312,11 +324,15 @@ def find_iterative(pair, iteratives):
1. first matched iterative 1. first matched iterative
2. second matched iterative 2. second matched iterative
""" """
iterative0: Optional[Iterative] = None
iterative1: Optional[Iterative] = None
for iterative in iteratives: for iterative in iteratives:
if iterative.group == pair[0]: if iterative.group == pair[0]:
iterative0 = iterative iterative0 = iterative
elif iterative.group == pair[1]: elif iterative.group == pair[1]:
iterative1 = iterative iterative1 = iterative
if iterative0 is None or iterative1 is None:
raise LookupError("iteratives not found")
return iterative0, iterative1 return iterative0, iterative1
@@ -327,7 +343,7 @@ class Iterative:
after the iterations are finished. after the iterations are finished.
""" """
def __init__(self, group): def __init__(self, group: Group):
"""Initialize object with group. """Initialize object with group.
Args: Args:
@@ -337,11 +353,15 @@ class Iterative:
self.atom = group.atom self.atom = group.atom
self.res_name = group.residue_type self.res_name = group.residue_type
self.q = group.charge self.q = group.charge
self.pka_old = None self.pka_old: Optional[float] = None
self.pka_new = None self.pka_new: Optional[float] = None
self.pka_iter = [] self.pka_iter: List[float] = []
self.pka_noniterative = 0.00 self.pka_noniterative = 0.00
self.determinants = {'sidechain': [], 'backbone': [], 'coulomb': []} self.determinants: Dict[str, list] = {
'sidechain': [],
'backbone': [],
'coulomb': []
}
self.group = group self.group = group
self.converged = True self.converged = True
# Calculate the Non-Iterative part of pKa from the group object # Calculate the Non-Iterative part of pKa from the group object
@@ -368,8 +388,9 @@ class Iterative:
self.pka_noniterative += coulomb self.pka_noniterative += coulomb
self.pka_old = self.pka_noniterative self.pka_old = self.pka_noniterative
def __eq__(self, other): def __eq__(self, other) -> bool:
"""Needed to use objects in sets.""" """Needed to use objects in sets."""
assert isinstance(other, (Iterative, Group)), type(other)
if self.atom.type == 'atom': if self.atom.type == 'atom':
# In case of protein atoms we trust the labels # In case of protein atoms we trust the labels
return self.label == other.label return self.label == other.label

View File

@@ -9,6 +9,10 @@ import sys
import logging import logging
import argparse import argparse
from pathlib import Path from pathlib import Path
from typing import List, TYPE_CHECKING
if TYPE_CHECKING:
from propka.atom import Atom
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -19,6 +23,8 @@ EXPECTED_ATOM_NUMBERS = {'ALA': 5, 'ARG': 11, 'ASN': 8, 'ASP': 8, 'CYS': 6,
'LEU': 8, 'LYS': 9, 'MET': 8, 'PHE': 11, 'PRO': 7, 'LEU': 8, 'LYS': 9, 'MET': 8, 'PHE': 11, 'PRO': 7,
'SER': 6, 'THR': 7, 'TRP': 14, 'TYR': 12, 'VAL': 7} 'SER': 6, 'THR': 7, 'TRP': 14, 'TYR': 12, 'VAL': 7}
Options = argparse.Namespace
def protein_precheck(conformations, names): def protein_precheck(conformations, names):
"""Check protein for correct number of atoms, etc. """Check protein for correct number of atoms, etc.
@@ -73,7 +79,7 @@ def resid_from_atom(atom):
atom.res_num, atom.chain_id, atom.icode) atom.res_num, atom.chain_id, atom.icode)
def split_atoms_into_molecules(atoms): def split_atoms_into_molecules(atoms: List["Atom"]):
"""Maps atoms into molecules. """Maps atoms into molecules.
Args: Args:
@@ -81,14 +87,14 @@ def split_atoms_into_molecules(atoms):
Returns: Returns:
list of molecules list of molecules
""" """
molecules = [] molecules: List[List["Atom"]] = []
while len(atoms) > 0: while len(atoms) > 0:
initial_atom = atoms.pop() initial_atom = atoms.pop()
molecules.append(make_molecule(initial_atom, atoms)) molecules.append(make_molecule(initial_atom, atoms))
return molecules return molecules
def make_molecule(atom, atoms): def make_molecule(atom: "Atom", atoms: List["Atom"]):
"""Make a molecule from atoms. """Make a molecule from atoms.
Args: Args:
@@ -302,7 +308,7 @@ def build_parser(parser=None):
return parser return parser
def loadOptions(args=None): def loadOptions(args=None) -> Options:
""" """
Load the arguments parser with options. Note that verbosity is set as soon Load the arguments parser with options. Note that verbosity is set as soon
as this function is invoked. as this function is invoked.
@@ -315,7 +321,7 @@ def loadOptions(args=None):
# loading the parser # loading the parser
parser = build_parser() parser = build_parser()
# parsing and returning options and arguments # parsing and returning options and arguments
options = parser.parse_args(args) options = parser.parse_args(args, namespace=Options())
# adding specified filenames to arguments # adding specified filenames to arguments
options.filenames.append(options.input_pdb) options.filenames.append(options.input_pdb)

View File

@@ -11,11 +11,13 @@ programs are required).
""" """
import logging import logging
import os import os
import shutil
import subprocess import subprocess
import sys import sys
import warnings import warnings
from propka.output import write_mol2_for_atoms from propka.output import write_mol2_for_atoms
from propka.lib import split_atoms_into_molecules from propka.lib import split_atoms_into_molecules
from propka.parameters import Parameters
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -24,7 +26,7 @@ _LOGGER = logging.getLogger(__name__)
class LigandPkaValues: class LigandPkaValues:
"""Ligand pKa value class.""" """Ligand pKa value class."""
def __init__(self, parameters): def __init__(self, parameters: Parameters):
"""Initialize object with parameters. """Initialize object with parameters.
Args: Args:
@@ -47,20 +49,16 @@ class LigandPkaValues:
Returns: Returns:
location of program location of program
""" """
path = os.environ.get('PATH').split(os.pathsep) loc = shutil.which(program)
locs = [ if loc is None:
i for i in filter(lambda loc: os.access(loc, os.F_OK),
map(lambda dir: os.path.join(dir, program),
path))]
if len(locs) == 0:
str_ = "'Error: Could not find {0:s}.".format(program) str_ = "'Error: Could not find {0:s}.".format(program)
str_ += ' Please make sure that it is found in the path.' str_ += ' Please make sure that it is found in the path.'
_LOGGER.info(str_) _LOGGER.info(str_)
sys.exit(-1) sys.exit(-1)
return locs[0] return loc
def get_marvin_pkas_for_pdb_file( def get_marvin_pkas_for_pdb_file(
self, molecule, parameters, num_pkas=10, min_ph=-10, max_ph=20): self, molecule, parameters, num_pkas=10, min_ph=-10.0, max_ph=20.0):
"""Use Marvin executables to get pKas for a PDB file. """Use Marvin executables to get pKas for a PDB file.
Args: Args:
@@ -75,7 +73,7 @@ class LigandPkaValues:
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)
def get_marvin_pkas_for_molecular_container(self, molecule, num_pkas=10, def get_marvin_pkas_for_molecular_container(self, molecule, num_pkas=10,
min_ph=-10, max_ph=20): min_ph=-10.0, max_ph=20.0):
"""Use Marvin executables to calculate pKas for a molecular container. """Use Marvin executables to calculate pKas for a molecular container.
Args: Args:
@@ -93,8 +91,8 @@ class LigandPkaValues:
def get_marvin_pkas_for_conformation_container(self, conformation, def get_marvin_pkas_for_conformation_container(self, conformation,
name='temp', reuse=False, name='temp', reuse=False,
num_pkas=10, min_ph=-10, num_pkas=10, min_ph=-10.0,
max_ph=20): max_ph=20.0):
"""Use Marvin executables to calculate pKas for a conformation container. """Use Marvin executables to calculate pKas for a conformation container.
Args: Args:
@@ -111,7 +109,7 @@ class LigandPkaValues:
num_pkas=num_pkas, min_ph=min_ph, max_ph=max_ph) num_pkas=num_pkas, min_ph=min_ph, max_ph=max_ph)
def get_marvin_pkas_for_atoms(self, atoms, name='temp', reuse=False, def get_marvin_pkas_for_atoms(self, atoms, name='temp', reuse=False,
num_pkas=10, min_ph=-10, max_ph=20): num_pkas=10, min_ph=-10.0, max_ph=20.0):
"""Use Marvin executables to calculate pKas for a list of atoms. """Use Marvin executables to calculate pKas for a list of atoms.
Args: Args:
@@ -131,8 +129,8 @@ class LigandPkaValues:
min_ph=min_ph, max_ph=max_ph) min_ph=min_ph, max_ph=max_ph)
def get_marvin_pkas_for_molecule(self, atoms, filename='__tmp_ligand.mol2', def get_marvin_pkas_for_molecule(self, atoms, filename='__tmp_ligand.mol2',
reuse=False, num_pkas=10, min_ph=-10, reuse=False, num_pkas=10, min_ph=-10.0,
max_ph=20): max_ph=20.0):
"""Use Marvin executables to calculate pKas for a molecule. """Use Marvin executables to calculate pKas for a molecule.
Args: Args:

View File

@@ -7,11 +7,12 @@ Molecular container for storing all contents of PDB files.
import logging import logging
import os import os
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
from propka.parameters import Parameters
import propka.version import propka.version
from propka.output import write_pka, print_header, print_result from propka.output import write_pka, print_header, print_result
from propka.conformation_container import ConformationContainer from propka.conformation_container import ConformationContainer
from propka.lib import make_grid from propka.lib import make_grid, Options
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -35,7 +36,7 @@ class MolecularContainer:
name: Optional[str] name: Optional[str]
version: propka.version.Version version: propka.version.Version
def __init__(self, parameters, options=None) -> None: def __init__(self, parameters: Parameters, options: Options) -> None:
"""Initialize molecular container. """Initialize molecular container.
Args: Args:

View File

@@ -365,7 +365,7 @@ def write_jackal_scap_file(mutation_data=None, filename="1xxx_scap.list",
for chain_id, _, res_num, code2 in mutation_data: for chain_id, _, res_num, code2 in mutation_data:
str_ = "{chain:s}, {num:d}, {code:s}\n".format( str_ = "{chain:s}, {num:d}, {code:s}\n".format(
chain=chain_id, num=res_num, code=code2) chain=chain_id, num=res_num, code=code2)
file_.write(str_) file_.write(str_)
def write_scwrl_sequence_file(sequence, filename="x-ray.seq", _=None): def write_scwrl_sequence_file(sequence, filename="x-ray.seq", _=None):

View File

@@ -11,67 +11,128 @@ in configuration file.
""" """
import logging import logging
from dataclasses import dataclass, field
from typing import Dict, List
try:
# New in version 3.10, deprecated since version 3.12
from typing import TypeAlias
except ImportError:
TypeAlias = "TypeAlias" # type: ignore
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
#: matrices class squared_property:
MATRICES = ['interaction_matrix']
#: pari-wise matrices def __set_name__(self, owner, name: str):
PAIR_WISE_MATRICES = ['sidechain_cutoffs'] assert name.endswith("_squared")
#: :class:`dict` containing numbers self._name_not_squared = name[:-len("_squared")] # removesuffix()
NUMBER_DICTIONARIES = [
'VanDerWaalsVolume', 'charge', 'model_pkas', 'ions', 'valence_electrons', def __get__(self, instance, owner=None) -> float:
'custom_model_pkas'] if instance is None:
#: :class:`dict` containing lists return self # type: ignore[return-value]
LIST_DICTIONARIES = ['backbone_NH_hydrogen_bond', 'backbone_CO_hydrogen_bond'] return getattr(instance, self._name_not_squared)**2
#: :class:`dict` containing strings
STRING_DICTIONARIES = ['protein_group_mapping'] def __set__(self, instance, value: float):
#: :class:`list` containing strings setattr(instance, self._name_not_squared, value**0.5)
STRING_LISTS = [
'ignore_residues', 'angular_dependent_sidechain_interactions',
'acid_list', 'base_list', 'exclude_sidechain_interactions',
'backbone_reorganisation_list', 'write_out_order']
#: distances (:class:`float`)
DISTANCES = ['desolv_cutoff', 'buried_cutoff', 'coulomb_cutoff1',
'coulomb_cutoff2']
#: other parameters
PARAMETERS = [
'Nmin', 'Nmax', 'desolvationSurfaceScalingFactor', 'desolvationPrefactor',
'desolvationAllowance', 'coulomb_diel', 'COO_HIS_exception',
'OCO_HIS_exception', 'CYS_HIS_exception', 'CYS_CYS_exception',
'min_ligand_model_pka', 'max_ligand_model_pka',
'include_H_in_interactions', 'coupling_max_number_of_bonds',
'min_bond_distance_for_hydrogen_bonds', 'coupling_penalty',
'shared_determinants', 'common_charge_centre', 'hide_penalised_group',
'remove_penalised_group', 'max_intrinsic_pka_diff',
'min_interaction_energy', 'max_free_energy_diff', 'min_swap_pka_shift',
'min_pka', 'max_pka', 'sidechain_interaction']
# :class:`str` parameters
STRINGS = ['version', 'output_file_tag', 'ligand_typing', 'pH', 'reference']
_T_MATRIX: TypeAlias = "InteractionMatrix"
_T_PAIR_WISE_MATRIX: TypeAlias = "PairwiseMatrix"
_T_NUMBER_DICTIONARY = Dict[str, float]
_T_LIST_DICTIONARY = Dict[str, list]
_T_STRING_DICTIONARY = Dict[str, str]
_T_STRING_LIST = List[str]
_T_STRING = str
_T_BOOL = bool
@dataclass
class Parameters: class Parameters:
"""PROPKA parameter class.""" """PROPKA parameter class."""
def __init__(self): # MATRICES
"""Initialize parameter class. interaction_matrix: _T_MATRIX = field(
default_factory=lambda: InteractionMatrix("interaction_matrix"))
Args: # PAIR_WISE_MATRICES
parameter_file: file with parameters sidechain_cutoffs: _T_PAIR_WISE_MATRIX = field(
""" default_factory=lambda: PairwiseMatrix("sidechain_cutoffs"))
# TODO - need to define all members explicitly
self.model_pkas = {} # NUMBER_DICTIONARIES
self.interaction_matrix = InteractionMatrix("interaction_matrix") VanDerWaalsVolume: _T_NUMBER_DICTIONARY = field(default_factory=dict)
self.sidechain_cutoffs = None charge: _T_NUMBER_DICTIONARY = field(default_factory=dict)
# TODO - it would be nice to rename these; they're defined everywhere model_pkas: _T_NUMBER_DICTIONARY = field(default_factory=dict)
self.COO_HIS_exception = None ions: _T_NUMBER_DICTIONARY = field(default_factory=dict)
self.OCO_HIS_exception = None valence_electrons: _T_NUMBER_DICTIONARY = field(default_factory=dict)
self.CYS_HIS_exception = None custom_model_pkas: _T_NUMBER_DICTIONARY = field(default_factory=dict)
self.CYS_CYS_exception = None
# These functions set up remaining data structures implicitly # LIST_DICTIONARIES
self.set_up_data_structures() backbone_NH_hydrogen_bond: _T_LIST_DICTIONARY = field(default_factory=dict)
backbone_CO_hydrogen_bond: _T_LIST_DICTIONARY = field(default_factory=dict)
# STRING_DICTIONARIES
protein_group_mapping: _T_STRING_DICTIONARY = field(default_factory=dict)
# STRING_LISTS
ignore_residues: _T_STRING_LIST = field(default_factory=list)
angular_dependent_sidechain_interactions: _T_STRING_LIST = field(default_factory=list)
acid_list: _T_STRING_LIST = field(default_factory=list)
base_list: _T_STRING_LIST = field(default_factory=list)
exclude_sidechain_interactions: _T_STRING_LIST = field(default_factory=list)
backbone_reorganisation_list: _T_STRING_LIST = field(default_factory=list)
write_out_order: _T_STRING_LIST = field(default_factory=list)
# DISTANCES
desolv_cutoff: float = 20.0
buried_cutoff: float = 15.0
coulomb_cutoff1: float = 4.0
coulomb_cutoff2: float = 10.0
# DISTANCES SQUARED
desolv_cutoff_squared = squared_property()
buried_cutoff_squared = squared_property()
coulomb_cutoff1_squared = squared_property()
coulomb_cutoff2_squared = squared_property()
# STRINGS
version: _T_STRING = "VersionA"
output_file_tag: _T_STRING = ""
ligand_typing: _T_STRING = "groups"
pH: _T_STRING = "variable"
reference: _T_STRING = "neutral"
# PARAMETERS
Nmin: int = 280
Nmax: int = 560
desolvationSurfaceScalingFactor: float = 0.25
desolvationPrefactor: float = -13.0
desolvationAllowance: float = 0.0
coulomb_diel: float = 80.0
# TODO - it would be nice to rename these; they're defined everywhere
COO_HIS_exception: float = 1.60
OCO_HIS_exception: float = 1.60
CYS_HIS_exception: float = 1.60
CYS_CYS_exception: float = 3.60
min_ligand_model_pka: float = -10.0
max_ligand_model_pka: float = 20.0
# include_H_in_interactions: NoReturn = None
coupling_max_number_of_bonds: int = 3
min_bond_distance_for_hydrogen_bonds: int = 4
# coupling_penalty: NoReturn = None
shared_determinants: _T_BOOL = False
common_charge_centre: _T_BOOL = False
# hide_penalised_group: NoReturn = None
remove_penalised_group: _T_BOOL = True
max_intrinsic_pka_diff: float = 2.0
min_interaction_energy: float = 0.5
max_free_energy_diff: float = 1.0
min_swap_pka_shift: float = 1.0
min_pka: float = 0.0
max_pka: float = 10.0
sidechain_interaction: float = 0.85
def parse_line(self, line): def parse_line(self, line):
"""Parse parameter file line.""" """Parse parameter file line."""
@@ -84,22 +145,21 @@ class Parameters:
if len(words) == 0: if len(words) == 0:
return return
# parse the words # parse the words
if len(words) == 3 and words[0] in NUMBER_DICTIONARIES: typeannotation = self.__annotations__.get(words[0])
if typeannotation is _T_NUMBER_DICTIONARY:
self.parse_to_number_dictionary(words) self.parse_to_number_dictionary(words)
elif len(words) == 2 and words[0] in STRING_LISTS: elif typeannotation is _T_STRING_LIST:
self.parse_to_string_list(words) self.parse_to_string_list(words)
elif len(words) == 2 and words[0] in DISTANCES: elif typeannotation is _T_STRING:
self.parse_distance(words)
elif len(words) == 2 and words[0] in PARAMETERS:
self.parse_parameter(words)
elif len(words) == 2 and words[0] in STRINGS:
self.parse_string(words) self.parse_string(words)
elif len(words) > 2 and words[0] in LIST_DICTIONARIES: elif typeannotation is _T_LIST_DICTIONARY:
self.parse_to_list_dictionary(words) self.parse_to_list_dictionary(words)
elif words[0] in MATRICES+PAIR_WISE_MATRICES: elif typeannotation is _T_MATRIX or typeannotation is _T_PAIR_WISE_MATRIX:
self.parse_to_matrix(words) self.parse_to_matrix(words)
elif len(words) == 3 and words[0] in STRING_DICTIONARIES: elif typeannotation is _T_STRING_DICTIONARY:
self.parse_to_string_dictionary(words) self.parse_to_string_dictionary(words)
else:
self.parse_parameter(words)
def parse_to_number_dictionary(self, words): def parse_to_number_dictionary(self, words):
"""Parse field to number dictionary. """Parse field to number dictionary.
@@ -107,6 +167,7 @@ class Parameters:
Args: Args:
words: strings to parse. words: strings to parse.
""" """
assert len(words) == 3, words
dict_ = getattr(self, words[0]) dict_ = getattr(self, words[0])
key = words[1] key = words[1]
value = words[2] value = words[2]
@@ -118,17 +179,19 @@ class Parameters:
Args: Args:
words: strings to parse words: strings to parse
""" """
assert len(words) == 3, words
dict_ = getattr(self, words[0]) dict_ = getattr(self, words[0])
key = words[1] key = words[1]
value = words[2] value = words[2]
dict_[key] = value dict_[key] = value
def parse_to_list_dictionary(self, words): def parse_to_list_dictionary(self, words: List[str]):
"""Parse field to list dictionary. """Parse field to list dictionary.
Args: Args:
words: strings to parse. words: strings to parse.
""" """
assert len(words) > 2, words
dict_ = getattr(self, words[0]) dict_ = getattr(self, words[0])
key = words[1] key = words[1]
if key not in dict_: if key not in dict_:
@@ -144,6 +207,7 @@ class Parameters:
Args: Args:
words: strings to parse words: strings to parse
""" """
assert len(words) == 2, words
list_ = getattr(self, words[0]) list_ = getattr(self, words[0])
value = words[1] value = words[1]
list_.append(value) list_.append(value)
@@ -158,24 +222,13 @@ class Parameters:
value = tuple(words[1:]) value = tuple(words[1:])
matrix.add(value) matrix.add(value)
def parse_distance(self, words):
"""Parse field to distance.
Args:
words: strings to parse
"""
value = float(words[1])
setattr(self, words[0], value)
value_sq = value*value
attr = "{0:s}_squared".format(words[0])
setattr(self, attr, value_sq)
def parse_parameter(self, words): def parse_parameter(self, words):
"""Parse field to parameters. """Parse field to parameters.
Args: Args:
words: strings to parse words: strings to parse
""" """
assert len(words) == 2, words
value = float(words[1]) value = float(words[1])
setattr(self, words[0], value) setattr(self, words[0], value)
@@ -185,28 +238,9 @@ class Parameters:
Args: Args:
words: strings to parse words: strings to parse
""" """
assert len(words) == 2, words
setattr(self, words[0], words[1]) setattr(self, words[0], words[1])
def set_up_data_structures(self):
"""Set up internal data structures.
TODO - it would be better to make these assignments explicit in
__init__.
"""
for key_word in (NUMBER_DICTIONARIES + LIST_DICTIONARIES
+ STRING_DICTIONARIES):
setattr(self, key_word, {})
for key_word in STRING_LISTS:
setattr(self, key_word, [])
for key_word in STRINGS:
setattr(self, key_word, "")
for key_word in MATRICES:
matrix = InteractionMatrix(key_word)
setattr(self, key_word, matrix)
for key_word in PAIR_WISE_MATRICES:
matrix = PairwiseMatrix(key_word)
setattr(self, key_word, matrix)
def print_interaction_parameters(self): def print_interaction_parameters(self):
"""Print interaction parameters.""" """Print interaction parameters."""
_LOGGER.info('--------------- Model pKa values ----------------------') _LOGGER.info('--------------- Model pKa values ----------------------')

View File

@@ -107,7 +107,7 @@ interaction_matrix SH I N N N N N N N N N N I I I I I I N N N N N N N N N N N I
sidechain_cutoffs default 3.0 4.0 sidechain_cutoffs default 3.0 4.0
# COO # COO
sidechain_cutoffs COO COO 2.5 3.5 sidechain_cutoffs COO COO 2.5 3.5
Sidechain_cutoffs COO SER 2.65 3.65 sidechain_cutoffs COO SER 2.65 3.65
sidechain_cutoffs COO ARG 1.85 2.85 sidechain_cutoffs COO ARG 1.85 2.85
sidechain_cutoffs COO LYS 2.85 3.85 sidechain_cutoffs COO LYS 2.85 3.85
sidechain_cutoffs COO HIS 2.0 3.0 sidechain_cutoffs COO HIS 2.0 3.0

View File

@@ -9,10 +9,16 @@ protons.
""" """
import logging import logging
import math import math
from typing import Iterable, TYPE_CHECKING
import propka.bonds import propka.bonds
import propka.atom import propka.atom
from propka.atom import Atom
from propka.vector_algebra import rotate_vector_around_an_axis, Vector from propka.vector_algebra import rotate_vector_around_an_axis, Vector
if TYPE_CHECKING:
from propka.molecular_container import MolecularContainer
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -58,7 +64,7 @@ class Protonate:
'Br': 1.41, 'I': 1.61, 'S': 1.35} 'Br': 1.41, 'I': 1.61, 'S': 1.35}
self.protonation_methods = {4: self.tetrahedral, 3: self.trigonal} self.protonation_methods = {4: self.tetrahedral, 3: self.trigonal}
def protonate(self, molecules): def protonate(self, molecules: "MolecularContainer"):
"""Protonate all atoms in the molecular container. """Protonate all atoms in the molecular container.
Args: Args:
@@ -75,7 +81,7 @@ class Protonate:
self.protonate_atom(atom) self.protonate_atom(atom)
@staticmethod @staticmethod
def remove_all_hydrogen_atoms(molecular_container): def remove_all_hydrogen_atoms(molecular_container: "MolecularContainer"):
"""Remove all hydrogen atoms from molecule. """Remove all hydrogen atoms from molecule.
Args: Args:
@@ -86,7 +92,7 @@ class Protonate:
molecular_container.conformations[name] molecular_container.conformations[name]
.get_non_hydrogen_atoms()) .get_non_hydrogen_atoms())
def set_charge(self, atom): def set_charge(self, atom: Atom):
"""Set charge for atom. """Set charge for atom.
Args: Args:
@@ -109,7 +115,7 @@ class Protonate:
atom.sybyl_type = atom.sybyl_type.replace('-', '') atom.sybyl_type = atom.sybyl_type.replace('-', '')
atom.charge_set = True atom.charge_set = True
def protonate_atom(self, atom): def protonate_atom(self, atom: Atom):
"""Protonate an atom. """Protonate an atom.
Args: Args:
@@ -126,7 +132,7 @@ class Protonate:
atom.is_protonated = True atom.is_protonated = True
@staticmethod @staticmethod
def set_proton_names(heavy_atoms): def set_proton_names(heavy_atoms: Iterable[Atom]):
"""Set names for protons. """Set names for protons.
Args: Args:
@@ -139,7 +145,7 @@ class Protonate:
bonded.name += str(i) bonded.name += str(i)
i += 1 i += 1
def set_number_of_protons_to_add(self, atom): def set_number_of_protons_to_add(self, atom: Atom):
"""Set the number of protons to add to this atom. """Set the number of protons to add to this atom.
Args: Args:
@@ -169,7 +175,7 @@ class Protonate:
_LOGGER.debug('-'*10) _LOGGER.debug('-'*10)
_LOGGER.debug(atom.number_of_protons_to_add) _LOGGER.debug(atom.number_of_protons_to_add)
def set_steric_number_and_lone_pairs(self, atom): def set_steric_number_and_lone_pairs(self, atom: Atom):
"""Set steric number and lone pairs for atom. """Set steric number and lone pairs for atom.
Args: Args:
@@ -207,8 +213,7 @@ class Protonate:
atom.steric_number += 0 atom.steric_number += 0
_LOGGER.debug('{0:>65s}: {1:>4.1f}'.format( _LOGGER.debug('{0:>65s}: {1:>4.1f}'.format(
'Charge(-)', atom.charge)) 'Charge(-)', atom.charge))
atom.steric_number -= atom.charge atom.steric_number = math.floor((atom.steric_number - atom.charge) / 2)
atom.steric_number = math.floor(atom.steric_number/2.0)
atom.number_of_lone_pairs = ( atom.number_of_lone_pairs = (
atom.steric_number - len(atom.bonded_atoms) atom.steric_number - len(atom.bonded_atoms)
- atom.number_of_protons_to_add - atom.number_of_protons_to_add
@@ -220,7 +225,7 @@ class Protonate:
'Number of lone pairs', atom.number_of_lone_pairs)) 'Number of lone pairs', atom.number_of_lone_pairs))
atom.steric_num_lone_pairs_set = True atom.steric_num_lone_pairs_set = True
def add_protons(self, atom): def add_protons(self, atom: Atom):
"""Add protons to atom. """Add protons to atom.
Args: Args:
@@ -236,7 +241,7 @@ class Protonate:
'(steric number: {0:d})'.format(atom.steric_number) '(steric number: {0:d})'.format(atom.steric_number)
) )
def trigonal(self, atom): def trigonal(self, atom: Atom):
"""Add hydrogens in trigonal geometry. """Add hydrogens in trigonal geometry.
Args: Args:
@@ -296,7 +301,7 @@ class Protonate:
new_a = self.set_bond_distance(new_a, atom.element) new_a = self.set_bond_distance(new_a, atom.element)
self.add_proton(atom, cvec+new_a) self.add_proton(atom, cvec+new_a)
def tetrahedral(self, atom): def tetrahedral(self, atom: Atom):
"""Protonate atom in tetrahedral geometry. """Protonate atom in tetrahedral geometry.
Args: Args:
@@ -338,13 +343,14 @@ class Protonate:
self.add_proton(atom, cvec+new_a) self.add_proton(atom, cvec+new_a)
@staticmethod @staticmethod
def add_proton(atom, position): def add_proton(atom: Atom, position: Vector):
"""Add a proton to an atom at a specific position. """Add a proton to an atom at a specific position.
Args: Args:
atom: atom to protonate atom: atom to protonate
position: position for proton position: position for proton
""" """
assert atom.conformation_container is not None
# Create the new proton # Create the new proton
new_h = propka.atom.Atom() new_h = propka.atom.Atom()
new_h.set_property( new_h.set_property(
@@ -368,7 +374,6 @@ class Protonate:
new_h.number_of_lone_pairs = 0 new_h.number_of_lone_pairs = 0
new_h.number_of_protons_to_add = 0 new_h.number_of_protons_to_add = 0
new_h.num_pi_elec_2_3_bonds = 0 new_h.num_pi_elec_2_3_bonds = 0
new_h.is_protonates = True
atom.bonded_atoms.append(new_h) atom.bonded_atoms.append(new_h)
atom.number_of_protons_to_add -= 1 atom.number_of_protons_to_add -= 1
atom.conformation_container.add_atom(new_h) atom.conformation_container.add_atom(new_h)
@@ -386,7 +391,7 @@ class Protonate:
i += 1 i += 1
_LOGGER.debug('added %s %s %s', new_h, 'to', atom) _LOGGER.debug('added %s %s %s', new_h, 'to', atom)
def set_bond_distance(self, bvec, element): def set_bond_distance(self, bvec: Vector, element: str) -> Vector:
"""Set bond distance between atom and element. """Set bond distance between atom and element.
Args: Args:

View File

@@ -6,7 +6,7 @@ Vector algebra for PROPKA.
""" """
import logging import logging
import math import math
from typing import Optional, Protocol, Union from typing import Optional, Protocol, overload
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -69,20 +69,30 @@ class Vector:
self.y - other.y, self.y - other.y,
self.z - other.z) self.z - other.z)
def __mul__(self, other: Union["Vector", "Matrix4x4", float]): def dot(self, other: _XYZ) -> float:
return self.x * other.x + self.y * other.y + self.z * other.z
@overload
def __mul__(self, other: "Vector") -> float:
...
@overload
def __mul__(self, other: "Matrix4x4") -> "Vector":
...
@overload
def __mul__(self, other: float) -> "Vector":
...
def __mul__(self, other):
"""Dot product, scalar and matrix multiplication.""" """Dot product, scalar and matrix multiplication."""
if isinstance(other, Vector): if isinstance(other, Vector):
return self.x * other.x + self.y * other.y + self.z * other.z # TODO deprecate in favor of self.dot()
elif isinstance(other, Matrix4x4): return self.dot(other)
return Vector( if isinstance(other, Matrix4x4):
xi=other.a11*self.x + other.a12*self.y + other.a13*self.z # TODO deprecate in favor of matmul operator
+ other.a14*1.0, return other @ self
yi=other.a21*self.x + other.a22*self.y + other.a23*self.z if isinstance(other, (int, float)):
+ other.a24*1.0,
zi=other.a31*self.x + other.a32*self.y + other.a33*self.z
+ other.a34*1.0
)
elif type(other) in [int, float]:
return Vector(self.x * other, self.y * other, self.z * other) return Vector(self.x * other, self.y * other, self.z * other)
raise TypeError(f'{type(other)} not supported') raise TypeError(f'{type(other)} not supported')
@@ -90,6 +100,10 @@ class Vector:
return self.__mul__(other) return self.__mul__(other)
def __pow__(self, other: _XYZ): def __pow__(self, other: _XYZ):
# TODO deprecate in favor of self.cross()
return self.cross(other)
def cross(self, other: _XYZ):
"""Cross product.""" """Cross product."""
return Vector(self.y * other.z - self.z * other.y, return Vector(self.y * other.z - self.z * other.y,
self.z * other.x - self.x * other.z, self.z * other.x - self.x * other.z,
@@ -160,6 +174,17 @@ class Matrix4x4:
self.a43 = a43i self.a43 = a43i
self.a44 = a44i self.a44 = a44i
def __matmul__(self, v: _XYZ) -> Vector:
"""Matrix vector multiplication with homogeneous coordinates.
Assumes that the last row is (0, 0, 0, 1).
"""
return Vector(
self.a11 * v.x + self.a12 * v.y + self.a13 * v.z + self.a14,
self.a21 * v.x + self.a22 * v.y + self.a23 * v.z + self.a24,
self.a31 * v.x + self.a32 * v.y + self.a33 * v.z + self.a34,
)
def angle(avec: Vector, bvec: Vector) -> float: def angle(avec: Vector, bvec: Vector) -> float:
"""Get the angle between two vectors. """Get the angle between two vectors.

View File

@@ -7,6 +7,7 @@ Contains version-specific methods and parameters.
TODO - this module unnecessarily confuses the code. Can we eliminate it? TODO - this module unnecessarily confuses the code. Can we eliminate it?
""" """
import logging import logging
from propka.atom import Atom
from propka.hydrogens import setup_bonding_and_protonation, setup_bonding from propka.hydrogens import setup_bonding_and_protonation, setup_bonding
from propka.hydrogens import setup_bonding_and_protonation_30_style from propka.hydrogens import setup_bonding_and_protonation_30_style
from propka.energy import radial_volume_desolvation, calculate_pair_weight from propka.energy import radial_volume_desolvation, calculate_pair_weight
@@ -98,6 +99,10 @@ class Version:
"""Setup bonding using assigned model.""" """Setup bonding using assigned model."""
return self.prepare_bonds(self.parameters, molecular_container) return self.prepare_bonds(self.parameters, molecular_container)
def get_hydrogen_bond_parameters(self, atom1: Atom, atom2: Atom) -> tuple:
"""Get hydrogen bond parameters for two atoms."""
raise NotImplementedError("abstract method")
class VersionA(Version): class VersionA(Version):
"""TODO - figure out what this is.""" """TODO - figure out what this is."""

View File

@@ -68,11 +68,11 @@ def run_propka(options, pdb_path, tmp_path):
""" """
options += [str(pdb_path)] options += [str(pdb_path)]
args = loadOptions(options) args = loadOptions(options)
cwd = Path.cwd()
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()
os.chdir(tmp_path) os.chdir(tmp_path)
parameters = read_parameter_file(args.parameters, Parameters()) parameters = read_parameter_file(args.parameters, Parameters())
molecule = MolecularContainer(parameters, args) molecule = MolecularContainer(parameters, args)
@@ -148,6 +148,7 @@ def compare_output(pdb, tmp_path, ref_path):
def test_regression(pdb, options, tmp_path): def test_regression(pdb, options, tmp_path):
"""Basic regression test of PROPKA functionality.""" """Basic regression test of PROPKA functionality."""
path_dict = get_test_dirs() path_dict = get_test_dirs()
ref_path = None
for ext in ["json", "dat"]: for ext in ["json", "dat"]:
ref_path = path_dict["results"] / f"{pdb}.{ext}" ref_path = path_dict["results"] / f"{pdb}.{ext}"

View File

@@ -0,0 +1,159 @@
import propka.vector_algebra as m
import math
import pytest
from pytest import approx
RADIANS_90 = math.pi / 2
def assert_vector_equal(v1: m.Vector, v2: m.Vector):
assert isinstance(v1, m.Vector)
assert isinstance(v2, m.Vector)
assert v1.x == approx(v2.x)
assert v1.y == approx(v2.y)
assert v1.z == approx(v2.z)
def _matrix4x4_tolist(self: m.Matrix4x4) -> list:
return [
self.a11, self.a12, self.a13, self.a14, self.a21, self.a22, self.a23, self.a24,
self.a31, self.a32, self.a33, self.a34, self.a41, self.a42, self.a43, self.a44
]
def assert_matrix4x4_equal(m1: m.Matrix4x4, m2: m.Matrix4x4):
assert isinstance(m1, m.Matrix4x4)
assert isinstance(m2, m.Matrix4x4)
assert _matrix4x4_tolist(m1) == approx(_matrix4x4_tolist(m2))
def test_Vector__init():
v = m.Vector()
assert v.x == 0.0
assert v.y == 0.0
assert v.z == 0.0
v = m.Vector(12, 34, 56)
assert v.x == 12
assert v.y == 34
assert v.z == 56
v1 = m.Vector(atom1=v)
assert v1.x == 12
assert v1.y == 34
assert v1.z == 56
v2 = m.Vector(5, 4, 3)
v3 = m.Vector(atom1=v2, atom2=v1)
assert v3.x == 7
assert v3.y == 30
assert v3.z == 53
def test_Vector__plusminus():
v1 = m.Vector(1, 2, 3)
v2 = m.Vector(4, 5, 6)
v3 = v1 + v2
assert v3.x == 5
assert v3.y == 7
assert v3.z == 9
v3 = v1 - v2
assert v3.x == -3
assert v3.y == -3
assert v3.z == -3
v4 = -v1
assert v4.x == -1
assert v4.y == -2
assert v4.z == -3
def test_Vector__mul__number():
v1 = m.Vector(1, 2, 3)
assert_vector_equal(v1 * 2, m.Vector(2, 4, 6))
def test_Vector__mul__Vector():
v1 = m.Vector(1, 2, 3)
v2 = m.Vector(4, 5, 6)
assert v1 * v2 == 32
assert v1.dot(v2) == 32
with pytest.raises(TypeError):
v1 @ v2 # type: ignore
def test_Vector__mul__Matrix4x4():
v1 = m.Vector(1, 2, 3)
assert_vector_equal(v1 * m.Matrix4x4(), m.Vector())
m2 = m.Matrix4x4(0, 1, 0, 0, 20, 0, 0, 0, 0, 0, 300, 0, 0, 0, 0, 1)
assert_vector_equal(v1 * m2, m.Vector(2, 20, 900))
assert_vector_equal(m2 * v1, m.Vector(2, 20, 900))
assert_vector_equal(m2 @ v1, m.Vector(2, 20, 900))
with pytest.raises(TypeError):
v1 @ m2 # type: ignore
def test_Vector__cross():
v1 = m.Vector(1, 2, 3)
v2 = m.Vector(4, 5, 6)
assert_vector_equal(v1**v2, m.Vector(-3, 6, -3)) # TODO deprecate
assert_vector_equal(v1.cross(v2), m.Vector(-3, 6, -3))
assert_vector_equal(v2.cross(v1), m.Vector(3, -6, 3))
def test_Vector__length():
v1 = m.Vector(1, 2, 3)
assert v1.length() == 14**0.5
assert v1.sq_length() == 14
def test_Vector__orthogonal():
v1 = m.Vector(1, 2, 3)
assert v1.dot(v1.orthogonal()) == 0
def test_Vector__rescale():
v1 = m.Vector(1, 2, 3)
v2 = v1.rescale(4)
assert v2.length() == 4
assert v2.x / v1.x == approx(4 / 14**0.5)
assert v2.y / v1.y == approx(4 / 14**0.5)
assert v2.z / v1.z == approx(4 / 14**0.5)
def test_angle():
v1 = m.Vector(0, 0, 1)
v2 = m.Vector(0, 2, 0)
assert m.angle(v1, v2) == RADIANS_90
def test_angle_degrees():
v1 = m.Vector(0, 0, 3)
v2 = m.Vector(5, 0, 0)
assert m.angle_degrees(v1, v2) == 90
def test_signed_angle_around_axis():
v1 = m.Vector(0, 0, 3)
v2 = m.Vector(5, 0, 0)
v3 = m.Vector(0, 1, 0)
assert m.signed_angle_around_axis(v1, v2, v3) == -RADIANS_90
v1 = m.Vector(0, 2, 3)
v2 = m.Vector(5, 4, 0)
assert m.signed_angle_around_axis(v1, v2, v3) == -RADIANS_90
def test_rotate_vector_around_an_axis():
v1 = m.Vector(0, 0, 3)
v2 = m.Vector(3, 0, 0)
v3 = m.Vector(0, -1, 0)
v4 = m.rotate_vector_around_an_axis(RADIANS_90, v3, v2)
assert_vector_equal(v4, v1)
def test_rotate_atoms_around_z_axis():
m_rot = m.rotate_atoms_around_z_axis(-RADIANS_90)
assert_matrix4x4_equal(m_rot,
m.Matrix4x4(0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1))
def test_rotate_atoms_around_y_axis():
m_rot = m.rotate_atoms_around_y_axis(RADIANS_90)
assert_matrix4x4_equal(m_rot,
m.Matrix4x4(0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 1))