aboutsummaryrefslogtreecommitdiff
path: root/plip/structure/detection.py
diff options
context:
space:
mode:
authorNavan Chauhan <navanchauhan@gmail.com>2020-07-02 20:48:33 +0530
committerNavan Chauhan <navanchauhan@gmail.com>2020-07-02 20:48:33 +0530
commit4be08f7bdd77991e9e453c1cda863c3f20c338d5 (patch)
tree083e8e91622221185a28fd50754abc2f86b1df43 /plip/structure/detection.py
initial commit
Diffstat (limited to 'plip/structure/detection.py')
-rw-r--r--plip/structure/detection.py479
1 files changed, 479 insertions, 0 deletions
diff --git a/plip/structure/detection.py b/plip/structure/detection.py
new file mode 100644
index 0000000..71fec63
--- /dev/null
+++ b/plip/structure/detection.py
@@ -0,0 +1,479 @@
+import itertools
+from collections import defaultdict
+from collections import namedtuple
+
+import numpy as np
+from openbabel.openbabel import OBAtomAtomIter
+
+from plip.basic import config, logger
+from plip.basic.supplemental import vecangle, vector, euclidean3d, projection
+from plip.basic.supplemental import whichresnumber, whichrestype, whichchain
+
+logger = logger.get_logger()
+
+def filter_contacts(pairings):
+ """Filter interactions by two criteria:
+ 1. No interactions between the same residue (important for intra mode).
+ 2. No duplicate interactions (A with B and B with A, also important for intra mode)."""
+ if not config.INTRA:
+ return pairings
+ filtered1_pairings = [p for p in pairings if (p.resnr, p.reschain) != (p.resnr_l, p.reschain_l)]
+ already_considered = []
+ filtered2_pairings = []
+ for contact in filtered1_pairings:
+ try:
+ dist = 'D{}'.format(round(contact.distance, 2))
+ except AttributeError:
+ try:
+ dist = 'D{}'.format(round(contact.distance_ah, 2))
+ except AttributeError:
+ dist = 'D{}'.format(round(contact.distance_aw, 2))
+ res1, res2 = ''.join([str(contact.resnr), contact.reschain]), ''.join(
+ [str(contact.resnr_l), contact.reschain_l])
+ data = {res1, res2, dist}
+ if data not in already_considered:
+ filtered2_pairings.append(contact)
+ already_considered.append(data)
+ return filtered2_pairings
+
+
+##################################################
+# FUNCTIONS FOR DETECTION OF SPECIFIC INTERACTIONS
+##################################################
+
+def hydrophobic_interactions(atom_set_a, atom_set_b):
+ """Detection of hydrophobic pliprofiler between atom_set_a (binding site) and atom_set_b (ligand).
+ Definition: All pairs of qualified carbon atoms within a distance of HYDROPH_DIST_MAX
+ """
+ data = namedtuple('hydroph_interaction', 'bsatom bsatom_orig_idx ligatom ligatom_orig_idx '
+ 'distance restype resnr reschain restype_l, resnr_l, reschain_l')
+ pairings = []
+ for a, b in itertools.product(atom_set_a, atom_set_b):
+ if a.orig_idx == b.orig_idx:
+ continue
+ e = euclidean3d(a.atom.coords, b.atom.coords)
+ if not config.MIN_DIST < e < config.HYDROPH_DIST_MAX:
+ continue
+ restype, resnr, reschain = whichrestype(a.atom), whichresnumber(a.atom), whichchain(a.atom)
+ restype_l, resnr_l, reschain_l = whichrestype(b.orig_atom), whichresnumber(b.orig_atom), whichchain(b.orig_atom)
+ contact = data(bsatom=a.atom, bsatom_orig_idx=a.orig_idx, ligatom=b.atom, ligatom_orig_idx=b.orig_idx,
+ distance=e, restype=restype, resnr=resnr,
+ reschain=reschain, restype_l=restype_l,
+ resnr_l=resnr_l, reschain_l=reschain_l)
+ pairings.append(contact)
+ return filter_contacts(pairings)
+
+
+def hbonds(acceptors, donor_pairs, protisdon, typ):
+ """Detection of hydrogen bonds between sets of acceptors and donor pairs.
+ Definition: All pairs of hydrogen bond acceptor and donors with
+ donor hydrogens and acceptor showing a distance within HBOND DIST MIN and HBOND DIST MAX
+ and donor angles above HBOND_DON_ANGLE_MIN
+ """
+ data = namedtuple('hbond', 'a a_orig_idx d d_orig_idx h distance_ah distance_ad angle type protisdon resnr '
+ 'restype reschain resnr_l restype_l reschain_l sidechain atype dtype')
+ pairings = []
+ for acc, don in itertools.product(acceptors, donor_pairs):
+ if not typ == 'strong':
+ continue
+ # Regular (strong) hydrogen bonds
+ dist_ah = euclidean3d(acc.a.coords, don.h.coords)
+ dist_ad = euclidean3d(acc.a.coords, don.d.coords)
+ if not config.MIN_DIST < dist_ad < config.HBOND_DIST_MAX:
+ continue
+ vec1, vec2 = vector(don.h.coords, don.d.coords), vector(don.h.coords, acc.a.coords)
+ v = vecangle(vec1, vec2)
+ if not v > config.HBOND_DON_ANGLE_MIN:
+ continue
+ protatom = don.d.OBAtom if protisdon else acc.a.OBAtom
+ ligatom = don.d.OBAtom if not protisdon else acc.a.OBAtom
+ is_sidechain_hbond = protatom.GetResidue().GetAtomProperty(protatom, 8) # Check if sidechain atom
+ resnr = whichresnumber(don.d) if protisdon else whichresnumber(acc.a)
+ resnr_l = whichresnumber(acc.a_orig_atom) if protisdon else whichresnumber(don.d_orig_atom)
+ restype = whichrestype(don.d) if protisdon else whichrestype(acc.a)
+ restype_l = whichrestype(acc.a_orig_atom) if protisdon else whichrestype(don.d_orig_atom)
+ reschain = whichchain(don.d) if protisdon else whichchain(acc.a)
+ rechain_l = whichchain(acc.a_orig_atom) if protisdon else whichchain(don.d_orig_atom)
+ # Next line prevents H-Bonds within amino acids in intermolecular interactions
+ if config.INTRA is not None and whichresnumber(don.d) == whichresnumber(acc.a):
+ continue
+ # Next line prevents backbone-backbone H-Bonds
+ if config.INTRA is not None and protatom.GetResidue().GetAtomProperty(protatom,
+ 8) and ligatom.GetResidue().GetAtomProperty(
+ ligatom, 8):
+ continue
+ contact = data(a=acc.a, a_orig_idx=acc.a_orig_idx, d=don.d, d_orig_idx=don.d_orig_idx, h=don.h,
+ distance_ah=dist_ah, distance_ad=dist_ad, angle=v, type=typ, protisdon=protisdon,
+ resnr=resnr, restype=restype, reschain=reschain, resnr_l=resnr_l,
+ restype_l=restype_l, reschain_l=rechain_l, sidechain=is_sidechain_hbond,
+ atype=acc.a.type, dtype=don.d.type)
+ pairings.append(contact)
+ return filter_contacts(pairings)
+
+
+def pistacking(rings_bs, rings_lig):
+ """Return all pi-stackings between the given aromatic ring systems in receptor and ligand."""
+ data = namedtuple(
+ 'pistack',
+ 'proteinring ligandring distance angle offset type restype resnr reschain restype_l resnr_l reschain_l')
+ pairings = []
+ for r, l in itertools.product(rings_bs, rings_lig):
+ # DISTANCE AND RING ANGLE CALCULATION
+ d = euclidean3d(r.center, l.center)
+ b = vecangle(r.normal, l.normal)
+ a = min(b, 180 - b if not 180 - b < 0 else b) # Smallest of two angles, depending on direction of normal
+
+ # RING CENTER OFFSET CALCULATION (project each ring center into the other ring)
+ proj1 = projection(l.normal, l.center, r.center)
+ proj2 = projection(r.normal, r.center, l.center)
+ offset = min(euclidean3d(proj1, l.center), euclidean3d(proj2, r.center))
+
+ # RECEPTOR DATA
+ resnr, restype, reschain = whichresnumber(r.atoms[0]), whichrestype(r.atoms[0]), whichchain(r.atoms[0])
+ resnr_l, restype_l, reschain_l = whichresnumber(l.orig_atoms[0]), whichrestype(
+ l.orig_atoms[0]), whichchain(l.orig_atoms[0])
+
+ # SELECTION BY DISTANCE, ANGLE AND OFFSET
+ passed = False
+ if not config.MIN_DIST < d < config.PISTACK_DIST_MAX:
+ continue
+ if 0 < a < config.PISTACK_ANG_DEV and offset < config.PISTACK_OFFSET_MAX:
+ ptype = 'P'
+ passed = True
+ if 90 - config.PISTACK_ANG_DEV < a < 90 + config.PISTACK_ANG_DEV and offset < config.PISTACK_OFFSET_MAX:
+ ptype = 'T'
+ passed = True
+ if passed:
+ contact = data(proteinring=r, ligandring=l, distance=d, angle=a, offset=offset,
+ type=ptype, resnr=resnr, restype=restype, reschain=reschain,
+ resnr_l=resnr_l, restype_l=restype_l, reschain_l=reschain_l)
+ pairings.append(contact)
+ return filter_contacts(pairings)
+
+
+def pication(rings, pos_charged, protcharged):
+ """Return all pi-Cation interaction between aromatic rings and positively charged groups.
+ For tertiary and quaternary amines, check also the angle between the ring and the nitrogen.
+ """
+ data = namedtuple(
+ 'pication', 'ring charge distance offset type restype resnr reschain restype_l resnr_l reschain_l protcharged')
+ pairings = []
+ if len(rings) == 0 or len(pos_charged) == 0:
+ return pairings
+ for ring in rings:
+ c = ring.center
+ for p in pos_charged:
+ d = euclidean3d(c, p.center)
+ # Project the center of charge into the ring and measure distance to ring center
+ proj = projection(ring.normal, ring.center, p.center)
+ offset = euclidean3d(proj, ring.center)
+ if not config.MIN_DIST < d < config.PICATION_DIST_MAX or not offset < config.PISTACK_OFFSET_MAX:
+ continue
+ if type(p).__name__ == 'lcharge' and p.fgroup == 'tertamine':
+ # Special case here if the ligand has a tertiary amine, check an additional angle
+ # Otherwise, we might have have a pi-cation interaction 'through' the ligand
+ n_atoms = [a_neighbor for a_neighbor in OBAtomAtomIter(p.atoms[0].OBAtom)]
+ n_atoms_coords = [(a.x(), a.y(), a.z()) for a in n_atoms]
+ amine_normal = np.cross(vector(n_atoms_coords[0], n_atoms_coords[1]),
+ vector(n_atoms_coords[2], n_atoms_coords[0]))
+ b = vecangle(ring.normal, amine_normal)
+ # Smallest of two angles, depending on direction of normal
+ a = min(b, 180 - b if not 180 - b < 0 else b)
+ if not a > 30.0:
+ resnr, restype = whichresnumber(ring.atoms[0]), whichrestype(ring.atoms[0])
+ reschain = whichchain(ring.atoms[0])
+ resnr_l, restype_l = whichresnumber(p.orig_atoms[0]), whichrestype(p.orig_atoms[0])
+ reschain_l = whichchain(p.orig_atoms[0])
+ contact = data(ring=ring, charge=p, distance=d, offset=offset, type='regular',
+ restype=restype, resnr=resnr, reschain=reschain,
+ restype_l=restype_l, resnr_l=resnr_l, reschain_l=reschain_l,
+ protcharged=protcharged)
+ pairings.append(contact)
+ break
+ resnr = whichresnumber(p.atoms[0]) if protcharged else whichresnumber(ring.atoms[0])
+ resnr_l = whichresnumber(ring.orig_atoms[0]) if protcharged else whichresnumber(p.orig_atoms[0])
+ restype = whichrestype(p.atoms[0]) if protcharged else whichrestype(ring.atoms[0])
+ restype_l = whichrestype(ring.orig_atoms[0]) if protcharged else whichrestype(p.orig_atoms[0])
+ reschain = whichchain(p.atoms[0]) if protcharged else whichchain(ring.atoms[0])
+ reschain_l = whichchain(ring.orig_atoms[0]) if protcharged else whichchain(p.orig_atoms[0])
+ contact = data(ring=ring, charge=p, distance=d, offset=offset, type='regular', restype=restype,
+ resnr=resnr, reschain=reschain, restype_l=restype_l, resnr_l=resnr_l,
+ reschain_l=reschain_l, protcharged=protcharged)
+ pairings.append(contact)
+ return filter_contacts(pairings)
+
+
+def saltbridge(poscenter, negcenter, protispos):
+ """Detect all salt bridges (pliprofiler between centers of positive and negative charge)"""
+ data = namedtuple(
+ 'saltbridge', 'positive negative distance protispos resnr restype reschain resnr_l restype_l reschain_l')
+ pairings = []
+ for pc, nc in itertools.product(poscenter, negcenter):
+ if not config.MIN_DIST < euclidean3d(pc.center, nc.center) < config.SALTBRIDGE_DIST_MAX:
+ continue
+ resnr = pc.resnr if protispos else nc.resnr
+ resnr_l = whichresnumber(nc.orig_atoms[0]) if protispos else whichresnumber(pc.orig_atoms[0])
+ restype = pc.restype if protispos else nc.restype
+ restype_l = whichrestype(nc.orig_atoms[0]) if protispos else whichrestype(pc.orig_atoms[0])
+ reschain = pc.reschain if protispos else nc.reschain
+ reschain_l = whichchain(nc.orig_atoms[0]) if protispos else whichchain(pc.orig_atoms[0])
+ contact = data(positive=pc, negative=nc, distance=euclidean3d(pc.center, nc.center), protispos=protispos,
+ resnr=resnr, restype=restype, reschain=reschain, resnr_l=resnr_l, restype_l=restype_l,
+ reschain_l=reschain_l)
+ pairings.append(contact)
+ return filter_contacts(pairings)
+
+
+def halogen(acceptor, donor):
+ """Detect all halogen bonds of the type Y-O...X-C"""
+ data = namedtuple('halogenbond', 'acc acc_orig_idx don don_orig_idx distance don_angle acc_angle restype '
+ 'resnr reschain restype_l resnr_l reschain_l donortype acctype sidechain')
+ pairings = []
+ for acc, don in itertools.product(acceptor, donor):
+ dist = euclidean3d(acc.o.coords, don.x.coords)
+ if not config.MIN_DIST < dist < config.HALOGEN_DIST_MAX:
+ continue
+ vec1, vec2 = vector(acc.o.coords, acc.y.coords), vector(acc.o.coords, don.x.coords)
+ vec3, vec4 = vector(don.x.coords, acc.o.coords), vector(don.x.coords, don.c.coords)
+ acc_angle, don_angle = vecangle(vec1, vec2), vecangle(vec3, vec4)
+ is_sidechain_hal = acc.o.OBAtom.GetResidue().GetAtomProperty(acc.o.OBAtom, 8) # Check if sidechain atom
+ if not config.HALOGEN_ACC_ANGLE - config.HALOGEN_ANGLE_DEV < acc_angle \
+ < config.HALOGEN_ACC_ANGLE + config.HALOGEN_ANGLE_DEV:
+ continue
+ if not config.HALOGEN_DON_ANGLE - config.HALOGEN_ANGLE_DEV < don_angle \
+ < config.HALOGEN_DON_ANGLE + config.HALOGEN_ANGLE_DEV:
+ continue
+ restype, reschain, resnr = whichrestype(acc.o), whichchain(acc.o), whichresnumber(acc.o)
+ restype_l, reschain_l, resnr_l = whichrestype(don.orig_x), whichchain(don.orig_x), whichresnumber(don.orig_x)
+ contact = data(acc=acc, acc_orig_idx=acc.o_orig_idx, don=don, don_orig_idx=don.x_orig_idx,
+ distance=dist, don_angle=don_angle, acc_angle=acc_angle,
+ restype=restype, resnr=resnr,
+ reschain=reschain, restype_l=restype_l,
+ reschain_l=reschain_l, resnr_l=resnr_l, donortype=don.x.OBAtom.GetType(), acctype=acc.o.type,
+ sidechain=is_sidechain_hal)
+ pairings.append(contact)
+ return filter_contacts(pairings)
+
+
+def water_bridges(bs_hba, lig_hba, bs_hbd, lig_hbd, water):
+ """Find water-bridged hydrogen bonds between ligand and protein. For now only considers bridged of first degree."""
+ data = namedtuple('waterbridge', 'a a_orig_idx atype d d_orig_idx dtype h water water_orig_idx distance_aw '
+ 'distance_dw d_angle w_angle type resnr restype reschain resnr_l restype_l reschain_l protisdon')
+ pairings = []
+ # First find all acceptor-water pairs with distance within d
+ # and all donor-water pairs with distance within d and angle greater theta
+ lig_aw, prot_aw, lig_dw, prot_hw = [], [], [], []
+ for w in water:
+ for acc1 in lig_hba:
+ dist = euclidean3d(acc1.a.coords, w.oxy.coords)
+ if config.WATER_BRIDGE_MINDIST <= dist <= config.WATER_BRIDGE_MAXDIST:
+ lig_aw.append((acc1, w, dist))
+ for acc2 in bs_hba:
+ dist = euclidean3d(acc2.a.coords, w.oxy.coords)
+ if config.WATER_BRIDGE_MINDIST <= dist <= config.WATER_BRIDGE_MAXDIST:
+ prot_aw.append((acc2, w, dist))
+ for don1 in lig_hbd:
+ dist = euclidean3d(don1.d.coords, w.oxy.coords)
+ d_angle = vecangle(vector(don1.h.coords, don1.d.coords), vector(don1.h.coords, w.oxy.coords))
+ if config.WATER_BRIDGE_MINDIST <= dist <= config.WATER_BRIDGE_MAXDIST \
+ and d_angle > config.WATER_BRIDGE_THETA_MIN:
+ lig_dw.append((don1, w, dist, d_angle))
+ for don2 in bs_hbd:
+ dist = euclidean3d(don2.d.coords, w.oxy.coords)
+ d_angle = vecangle(vector(don2.h.coords, don2.d.coords), vector(don2.h.coords, w.oxy.coords))
+ if config.WATER_BRIDGE_MINDIST <= dist <= config.WATER_BRIDGE_MAXDIST \
+ and d_angle > config.WATER_BRIDGE_THETA_MIN:
+ prot_hw.append((don2, w, dist, d_angle))
+
+ for l, p in itertools.product(lig_aw, prot_hw):
+ acc, wl, distance_aw = l
+ don, wd, distance_dw, d_angle = p
+ if not wl.oxy == wd.oxy:
+ continue
+ # Same water molecule and angle within omega
+ w_angle = vecangle(vector(acc.a.coords, wl.oxy.coords), vector(wl.oxy.coords, don.h.coords))
+ if not config.WATER_BRIDGE_OMEGA_MIN < w_angle < config.WATER_BRIDGE_OMEGA_MAX:
+ continue
+ resnr, reschain, restype = whichresnumber(don.d), whichchain(don.d), whichrestype(don.d)
+ resnr_l, reschain_l, restype_l = whichresnumber(acc.a_orig_atom), whichchain(
+ acc.a_orig_atom), whichrestype(acc.a_orig_atom)
+ contact = data(a=acc.a, a_orig_idx=acc.a_orig_idx, atype=acc.a.type, d=don.d, d_orig_idx=don.d_orig_idx,
+ dtype=don.d.type, h=don.h, water=wl.oxy, water_orig_idx=wl.oxy_orig_idx,
+ distance_aw=distance_aw, distance_dw=distance_dw, d_angle=d_angle, w_angle=w_angle,
+ type='first_deg', resnr=resnr, restype=restype,
+ reschain=reschain, restype_l=restype_l, resnr_l=resnr_l, reschain_l=reschain_l, protisdon=True)
+ pairings.append(contact)
+ for p, l in itertools.product(prot_aw, lig_dw):
+ acc, wl, distance_aw = p
+ don, wd, distance_dw, d_angle = l
+ if not wl.oxy == wd.oxy:
+ continue
+ # Same water molecule and angle within omega
+ w_angle = vecangle(vector(acc.a.coords, wl.oxy.coords), vector(wl.oxy.coords, don.h.coords))
+ if not config.WATER_BRIDGE_OMEGA_MIN < w_angle < config.WATER_BRIDGE_OMEGA_MAX:
+ continue
+ resnr, reschain, restype = whichresnumber(acc.a), whichchain(acc.a), whichrestype(acc.a)
+ resnr_l, reschain_l, restype_l = whichresnumber(don.d_orig_atom), whichchain(
+ don.d_orig_atom), whichrestype(don.d_orig_atom)
+ contact = data(a=acc.a, a_orig_idx=acc.a_orig_idx, atype=acc.a.type, d=don.d, d_orig_idx=don.d_orig_idx,
+ dtype=don.d.type, h=don.h, water=wl.oxy, water_orig_idx=wl.oxy_orig_idx,
+ distance_aw=distance_aw, distance_dw=distance_dw,
+ d_angle=d_angle, w_angle=w_angle, type='first_deg', resnr=resnr,
+ restype=restype, reschain=reschain,
+ restype_l=restype_l, reschain_l=reschain_l, resnr_l=resnr_l, protisdon=False)
+ pairings.append(contact)
+ return filter_contacts(pairings)
+
+
+def metal_complexation(metals, metal_binding_lig, metal_binding_bs):
+ """Find all metal complexes between metals and appropriate groups in both protein and ligand, as well as water"""
+ data = namedtuple('metal_complex', 'metal metal_orig_idx metal_type target target_orig_idx target_type '
+ 'coordination_num distance resnr restype '
+ 'reschain restype_l reschain_l resnr_l location rms, geometry num_partners complexnum')
+ pairings_dict = {}
+ pairings = []
+ # #@todo Refactor
+ metal_to_id = {}
+ metal_to_orig_atom = {}
+ for metal, target in itertools.product(metals, metal_binding_lig + metal_binding_bs):
+ distance = euclidean3d(metal.m.coords, target.atom.coords)
+ if not distance < config.METAL_DIST_MAX:
+ continue
+ if metal.m not in pairings_dict:
+ pairings_dict[metal.m] = [(target, distance), ]
+ metal_to_id[metal.m] = metal.m_orig_idx
+ metal_to_orig_atom[metal.m] = metal.orig_m
+ else:
+ pairings_dict[metal.m].append((target, distance))
+ for cnum, metal in enumerate(pairings_dict):
+ rms = 0.0
+ excluded = []
+ # cnum +1 being the complex number
+ contact_pairs = pairings_dict[metal]
+ num_targets = len(contact_pairs)
+ vectors_dict = defaultdict(list)
+ for contact_pair in contact_pairs:
+ target, distance = contact_pair
+ vectors_dict[target.atom.idx].append(vector(metal.coords, target.atom.coords))
+
+ # Listing of coordination numbers and their geometries
+ configs = {2: ['linear', ],
+ 3: ['trigonal.planar', 'trigonal.pyramidal'],
+ 4: ['tetrahedral', 'square.planar'],
+ 5: ['trigonal.bipyramidal', 'square.pyramidal'],
+ 6: ['octahedral', ]}
+
+ # Angle signatures for each geometry (as seen from each target atom)
+ ideal_angles = {'linear': [[180.0]] * 2,
+ 'trigonal.planar': [[120.0, 120.0]] * 3,
+ 'trigonal.pyramidal': [[109.5, 109.5]] * 3,
+ 'tetrahedral': [[109.5, 109.5, 109.5, 109.5]] * 4,
+ 'square.planar': [[90.0, 90.0, 90.0, 90.0]] * 4,
+ 'trigonal.bipyramidal': [[120.0, 120.0, 90.0, 90.0]] * 3 + [[90.0, 90.0, 90.0, 180.0]] * 2,
+ 'square.pyramidal': [[90.0, 90.0, 90.0, 180.0]] * 4 + [[90.0, 90.0, 90.0, 90.0]],
+ 'octahedral': [[90.0, 90.0, 90.0, 90.0, 180.0]] * 6}
+ angles_dict = {}
+
+ for target in vectors_dict:
+ cur_vector = vectors_dict[target]
+ other_vectors = []
+ for t in vectors_dict:
+ if not t == target:
+ [other_vectors.append(x) for x in vectors_dict[t]]
+ angles = [vecangle(pair[0], pair[1]) for pair in itertools.product(cur_vector, other_vectors)]
+ angles_dict[target] = angles
+
+ all_total = [] # Record fit information for each geometry tested
+ gdata = namedtuple('gdata', 'geometry rms coordination excluded diff_targets') # Geometry Data
+ # Can't specify geometry with only one target
+ if num_targets == 1:
+ final_geom = 'NA'
+ final_coo = 1
+ excluded = []
+ rms = 0.0
+ else:
+ for coo in sorted(configs, reverse=True): # Start with highest coordination number
+ geometries = configs[coo]
+ for geometry in geometries:
+ signature = ideal_angles[geometry] # Set of ideal angles for geometry, from each perspective
+ geometry_total = 0
+ geometry_scores = [] # All scores for one geometry (from all subsignatures)
+ used_up_targets = [] # Use each target just once for a subsignature
+ not_used = []
+ coo_diff = num_targets - coo # How many more observed targets are there?
+
+ # Find best match for each subsignature
+ for subsignature in signature: # Ideal angles from one perspective
+ best_target = None # There's one best-matching target for each subsignature
+ best_target_score = 999
+
+ for k, target in enumerate(angles_dict):
+ if target not in used_up_targets:
+ observed_angles = angles_dict[target] # Observed angles from perspective of one target
+ single_target_scores = []
+ used_up_observed_angles = []
+ for i, ideal_angle in enumerate(subsignature):
+ # For each angle in the signature, find the best-matching observed angle
+ best_match = None
+ best_match_diff = 999
+ for j, observed_angle in enumerate(observed_angles):
+ if j not in used_up_observed_angles:
+ diff = abs(ideal_angle - observed_angle)
+ if diff < best_match_diff:
+ best_match_diff = diff
+ best_match = j
+ if best_match is not None:
+ used_up_observed_angles.append(best_match)
+ single_target_scores.append(best_match_diff)
+ # Calculate RMS for target angles
+ target_total = sum([x ** 2 for x in single_target_scores]) ** 0.5 # Tot. score targ/sig
+ if target_total < best_target_score:
+ best_target_score = target_total
+ best_target = target
+
+ used_up_targets.append(best_target)
+ geometry_scores.append(best_target_score)
+ # Total score is mean of RMS values
+ geometry_total = np.mean(geometry_scores)
+ # Record the targets not used for excluding them when deciding for a final geometry
+ [not_used.append(target) for target in angles_dict if target not in used_up_targets]
+ all_total.append(gdata(geometry=geometry, rms=geometry_total, coordination=coo,
+ excluded=not_used, diff_targets=coo_diff))
+
+ # Make a decision here. Starting with the geometry with lowest difference in ideal and observed partners ...
+ # Check if the difference between the RMS to the next best solution is not larger than 0.5
+ if not num_targets == 1: # Can't decide for any geoemtry in that case
+ all_total = sorted(all_total, key=lambda x: abs(x.diff_targets))
+ for i, total in enumerate(all_total):
+ next_total = all_total[i + 1]
+ this_rms, next_rms = total.rms, next_total.rms
+ diff_to_next = next_rms - this_rms
+ if diff_to_next > 0.5:
+ final_geom, final_coo, rms, excluded = total.geometry, total.coordination, total.rms, total.excluded
+ break
+ elif next_total.rms < 3.5:
+ final_geom, final_coo, = next_total.geometry, next_total.coordination
+ rms, excluded = next_total.rms, next_total.excluded
+ break
+ elif i == len(all_total) - 2:
+ final_geom, final_coo, rms, excluded = "NA", "NA", float('nan'), []
+ break
+
+ # Record all contact pairing, excluding those with targets superfluous for chosen geometry
+ only_water = set([x[0].location for x in contact_pairs]) == {'water'}
+ if not only_water: # No complex if just with water as targets
+ logger.info(f'metal ion {metal.type} complexed with {final_geom} geometry (coo. number {final_coo}/ {num_targets} observed)')
+ for contact_pair in contact_pairs:
+ target, distance = contact_pair
+ if target.atom.idx not in excluded:
+ metal_orig_atom = metal_to_orig_atom[metal]
+ restype_l, reschain_l, resnr_l = whichrestype(metal_orig_atom), whichchain(
+ metal_orig_atom), whichresnumber(metal_orig_atom)
+ contact = data(metal=metal, metal_orig_idx=metal_to_id[metal], metal_type=metal.type,
+ target=target, target_orig_idx=target.atom_orig_idx, target_type=target.type,
+ coordination_num=final_coo, distance=distance, resnr=target.resnr,
+ restype=target.restype, reschain=target.reschain, location=target.location,
+ rms=rms, geometry=final_geom, num_partners=num_targets, complexnum=cnum + 1,
+ resnr_l=resnr_l, restype_l=restype_l, reschain_l=reschain_l)
+ pairings.append(contact)
+ return filter_contacts(pairings)