def test_upsion_tau_acceptor(donor, donor_hyds, acceptor, r2, upsilon_low, upsilon_high, theta, tau, tau_sym): dc = donor._hb_coord ac = acceptor._hb_coord d2 = distance_squared(dc, ac) if d2 > r2: if hbond.verbose: print("dist criteria failed (%g > %g)" % (sqrt(d2), sqrt(r2))) return False upsilon_high = 0 - upsilon_high heavys = [a for a in donor.neighbors if a.element.number > 1] if len(heavys) != 1: raise AtomTypeError("upsilon tau donor (%s) not bonded to" " exactly one heavy atom" % donor) ang = angle(heavys[0]._hb_coord, dc, ac) if ang < upsilon_low or ang > upsilon_high: if hbond.verbose: print("upsilon criteria failed (%g < %g or %g > %g)" % (ang, upsilon_low, ang, upsilon_high)) return False if hbond.verbose: print("upsilon criteria OK (%g < %g < %g)" % (upsilon_low, ang, upsilon_high)) dp = dc ap = ac if not test_theta(dp, donor_hyds, ap, theta): return False return test_tau(tau, tau_sym, donor, dp, ap)
def build_next_atom_from_geometry(residue, residue_anchor, template_anchor, template_new_atom): from chimerax.atomic import struct_edit from chimerax.geometry import distance, angle, dihedral r = residue m = r.structure tnext = template_new_atom if tnext is None: raise TypeError('Template does not contain an atom with that name!') tstub = template_anchor rstub = residue_anchor existing_rstub_neighbors = rstub.neighbors n1 = rstub n2 = n3 = None t_direct_neighbors = [] r_direct_neighbors = [] for a2 in tstub.neighbors: if a2.element.name != 'H': n2 = r.find_atom(a2.name) if n2: t_direct_neighbors.append(a2) r_direct_neighbors.append(n2) if len(t_direct_neighbors) > 1: a2, a3 = t_direct_neighbors[:2] n2, n3 = r_direct_neighbors[:2] else: a2 = t_direct_neighbors[0] n2 = r_direct_neighbors[0] if not n2: raise TypeError( 'No n2 found - Not enough connected atoms to form a dihedral!') if not n3: for a3 in a2.neighbors: if a3 not in (a2, tstub) and a3.element.name != 'H': n3 = r.find_atom(a3.name) if n3: break if not n3: raise TypeError( 'No n3 found - Not enough connected atoms to form a dihedral!') # print('Building next atom {} from geometry of {}'.format(template_new_atom.name, # ','.join([n.name for n in (n1, n2, n3)]))) dist = distance(tnext.coord, tstub.coord) ang = angle(tnext.coord, tstub.coord, a2.coord) dihe = dihedral(tnext.coord, tstub.coord, a2.coord, a3.coord) # print('{}: {} {} {}'.format(next_atom_name, dist, ang, dihe)) a = struct_edit.add_dihedral_atom(tnext.name, tnext.element, n1, n2, n3, dist, ang, dihe) a.occupancy = rstub.occupancy a.bfactor = rstub.bfactor return a
def interpolate_dihedral(i0, i1, i2, i3, coords0, coords1, f, coord_set): """ Computer coordinate of atom a0 by interpolating dihedral angle defined by atoms (a0, a1, a2, a3). """ t0 = time() from chimerax.geometry import distance, angle, dihedral, dihedral_point c00 = coords0[i0] c01 = coords0[i1] c02 = coords0[i2] c03 = coords0[i3] length0 = distance(c00, c01) angle0 = angle(c00, c01, c02) dihed0 = dihedral(c00, c01, c02, c03) c10 = coords1[i0] c11 = coords1[i1] c12 = coords1[i2] c13 = coords1[i3] length1 = distance(c10, c11) angle1 = angle(c10, c11, c12) dihed1 = dihedral(c10, c11, c12, c13) length = length0 + (length1 - length0) * f angle = angle0 + (angle1 - angle0) * f ddihed = dihed1 - dihed0 if ddihed > 180: ddihed -= 360 elif ddihed < -180: ddihed += 360 dihed = dihed0 + ddihed * f c1 = coord_set[i1, :] c2 = coord_set[i2, :] c3 = coord_set[i3, :] t2 = time() c0 = dihedral_point(c1, c2, c3, length, angle, dihed) t3 = time() coord_set[i0:] = c0 t1 = time() global iit, dpt iit += t1 - t0 dpt += t3 - t2
def metal_clash(metal_pos, pos, parent_pos, parent_atom, parent_type_info): if parent_atom.element.valence < 5 and parent_type_info.geometry != linear: # non-sp1 carbons, et al, can't coordinate metals return False from chimerax.geometry import distance, angle if distance(metal_pos, pos) > 2.7: # "_metal_dist" is 2.7 + putative S-H bond length of 1.25; # see nitrogen stripping in CYS 77 and 120 in 3r24 return False # 135.0 is not strict enough (see :1004.a in 1nyr) if angle(parent_pos, pos, metal_pos) > 120.0: return True return False
def _len_angle(new, n1, n2, template, bond_cache, angle_cache): from chimerax.geometry import distance, angle bond_key = (n1, new) angle_key = (n2, n1, new) try: bl = bond_cache[bond_key] ang = angle_cache[angle_key] except KeyError: n2pos = template.find_atom(n2).coord n1pos = template.find_atom(n1).coord newpos = template.find_atom(new).coord bond_cache[bond_key] = bl = distance(newpos, n1pos) angle_cache[angle_key] = ang = angle(newpos, n1pos, n2pos) return bl, ang
def test_theta(dp, donor_hyds, ap, theta): if len(donor_hyds) == 0: if hbond.verbose: print("no hydrogens for theta test; default accept") return True for hyd_pos in donor_hyds: ang = angle(ap, hyd_pos, dp) if ang >= theta: if hbond.verbose: print("theta okay (%g >= %g)" % (ang, theta)) return True if hbond.verbose: print("theta failure (%g < %g)" % (ang, theta)) return False
def _extend_ends(self, centers, frac): n = len(centers) if self.curved: tc = self.center # Torus center r0,r1 = centers[0] - tc, centers[-1] - tc # radial vectors from center of torus. from chimerax.geometry import angle, rotation a = frac * angle(r0,r1) / (n-1) c0 = tc + rotation(self.axis, -a) * r0 c1 = tc + rotation(self.axis, a) * r1 else: e = frac/(n-1) * (centers[-1] - centers[0]) c0 = centers[0] - e c1 = centers[-1] + e n = len(centers) from numpy import concatenate ecenters = concatenate((c0.reshape(1,3), centers, c1.reshape(1,3))) return ecenters
def test_phi(dp, ap, bp, phi_plane, phi): if phi_plane: normal = normalize_vector( cross_product(phi_plane[1] - phi_plane[0], phi_plane[2] - phi_plane[1])) D = dot(normal, phi_plane[1]) bproj = project(bp, normal, D) aproj = project(ap, normal, D) dproj = project(dp, normal, D) ang = angle(bproj, aproj, dproj) if ang < phi: if hbond.verbose: print("phi criteria failed (%g < %g)" % (ang, phi)) return False if hbond.verbose: print("phi criteria OK (%g >= %g)" % (ang, phi)) else: if hbond.verbose: print("phi criteria irrelevant") return True
def form_dihedral(res_bud, real1, tmpl_res, a, b, pos=None, dihed=None): from chimerax.atomic.struct_edit import add_atom, add_dihedral_atom res = res_bud.residue if pos: return add_atom(a.name, a.element, res, pos, info_from=real1) # use neighbors of res_bud rather than real1 to avoid clashes with # other res_bud neighbors in case bond to real1 neighbor freely rotates inres = [ nb for nb in res_bud.neighbors if nb != real1 and nb.residue == res ] if len(inres) < 1: inres = [x for x in res.atoms if x not in [res_bud, real1]] if real1.residue != res or len(inres) < 1: raise AssertionError( "Can't form in-residue dihedral for %s of residue %s" % (res_bud, res)) if dihed: real1 = res.find_atom("C1'") real2 = res.find_atom("O4'") else: real2 = inres[0] xyz0, xyz1, xyz2 = [ tmpl_res.find_atom(a.name).coord for a in (res_bud, real1, real2) ] xyz = a.coord blen = b.length from chimerax.geometry import angle, dihedral ang = angle(xyz, xyz0, xyz1) if dihed is None: dihed = dihedral(xyz, xyz0, xyz1, xyz2) return add_dihedral_atom(a.name, a.element, res_bud, real1, real2, blen, ang, dihed, info_from=real1)
def don_generic(donor, donor_hyds, acceptor, sp2_O_rp2, sp3_O_rp2, sp3_N_rp2, sp2_O_r2, sp3_O_r2, sp3_N_r2, gen_rp2, gen_r2, min_hyd_angle, min_bonded_angle): if hbond.verbose: print("don_generic") dc = donor._hb_coord ac = acceptor._hb_coord acc_type = acceptor.idatm_type if acc_type not in type_info: return False geom = type_info[acc_type].geometry element = acceptor.element.name if element == 'O' and geom == planar: if hbond.verbose: print("planar O") r2 = sp2_O_r2 rp2 = sp2_O_rp2 elif element == 'O' and geom == tetrahedral or element == 'N' and geom == planar: if hbond.verbose: print("planar N or tet O") r2 = sp3_O_r2 rp2 = sp3_O_rp2 elif element == 'N' and geom == tetrahedral: if hbond.verbose: print("tet N") r2 = sp3_N_r2 rp2 = sp3_N_rp2 else: if hbond.verbose: print("generic acceptor") if acceptor.element.name == "S": r2 = sulphur_compensate(gen_r2) min_bonded_angle = min_bonded_angle - 9 r2 = gen_r2 rp2 = gen_rp2 ap = acceptor._hb_coord dp = donor._hb_coord if len(donor_hyds) == 0: d2 = distance_squared(dc, ac) if d2 > r2: if hbond.verbose: print("dist criteria failed (%g > %g)" % (sqrt(d2), sqrt(r2))) return False else: for hyd_pos in donor_hyds: if distance_squared(hyd_pos, ap) < rp2: break else: if hbond.verbose: print("hyd dist criteria failed (all >= %g)" % (sqrt(rp2))) return False if hbond.verbose: print("dist criteria OK") for bonded in donor.neighbors: if bonded.element.number <= 1: continue bp = bonded._hb_coord ang = angle(bp, dp, ap) if ang < min_bonded_angle: if hbond.verbose: print("bonded angle too sharp (%g < %g)" % (ang, min_bonded_angle)) return False if len(donor_hyds) == 0: if hbond.verbose: print("No specific hydrogen positions; default accept") return True for hyd_pos in donor_hyds: ang = angle(dp, hyd_pos, ap) if ang >= min_hyd_angle: if hbond.verbose: print("hydrogen angle okay (%g >= %g)" % (ang, min_hyd_angle)) return True if hbond.verbose: print("hydrogen angle(s) too sharp (< %g)" % min_hyd_angle) return False
def don_theta_tau(donor, donor_hyds, acceptor, sp2_O_rp2, sp2_O_theta, sp3_O_rp2, sp3_O_theta, sp3_O_phi, sp3_N_rp2, sp3_N_theta, sp3_N_upsilon, gen_rp2, gen_theta, is_water=False): # 'is_water' only for hydrogenless water if hbond.verbose: print("don_theta_tau") if len(donor_hyds) == 0 and not is_water: if hbond.verbose: print("No hydrogens; default failure") return False ap = acceptor._hb_coord dp = donor._hb_coord acc_type = acceptor.idatm_type if acc_type not in type_info: if hbond.verbose: print("Unknown acceptor type failure") return False geom = type_info[acc_type].geometry element = acceptor.element.name if element == 'O' and geom == planar: if hbond.verbose: print("planar O") for hyd_pos in donor_hyds: if distance_squared(hyd_pos, ap) <= sp2_O_rp2: break else: if not is_water: if hbond.verbose: print("dist criteria failed (all > %g)"% sqrt(sp2_O_rp2)) return False theta = sp2_O_theta elif element == 'O' and geom == tetrahedral or element == 'N' and geom == planar: if hbond.verbose: print("planar N or tet O") for hyd_pos in donor_hyds: if distance_squared(hyd_pos, ap) <= sp3_O_rp2: break else: if not is_water: if hbond.verbose: print("dist criteria failed (all > %g)" % sqrt(sp3_O_rp2)) return False theta = sp3_O_theta # only test phi for acceptors with two bonded atoms if acceptor.num_bonds == 2: if hbond.verbose: print("testing donor phi") bonded = acceptor.neighbors phi_plane, base_pos = get_phi_plane_params(acceptor, bonded[0], bonded[1]) if not test_phi(donor._hb_coord, ap, base_pos, phi_plane, sp3_O_phi): return False elif element == 'N' and geom == tetrahedral: if hbond.verbose: print("tet N") for hyd_pos in donor_hyds: if distance_squared(hyd_pos, ap) <= sp3_N_rp2: break else: if not is_water: if hbond.verbose: print("dist criteria failed (all > %g)" % sqrt(sp3_N_rp2)) return False theta = sp3_N_theta # test upsilon against lone pair directions bonded_pos = [] for bonded in acceptor.neighbors: bonded_pos.append(bonded._hb_coord) lp_pos = bond_positions(ap, geom, 1.0, bonded_pos) if len(lp_pos) > 0: # fixed lone pair positions for lp in bond_positions(ap, geom, 1.0, bonded_pos): # invert position so that we are measuring angles correctly ang = angle(dp, ap, ap - (lp - ap)) if ang > sp3_N_upsilon: if hbond.verbose: print("acceptor upsilon okay (%g > %g)" % (ang, sp3_N_upsilon)) break else: if hbond.verbose: print("all acceptor upsilons failed (< %g)" % sp3_N_upsilon) return False # else: indefinite lone pair positions; default okay else: if hbond.verbose: print("generic acceptor") if acceptor.element.name == "S": gen_rp2 = sulphur_compensate(gen_rp2) for hyd_pos in donor_hyds: if distance_squared(hyd_pos, ap) <= gen_rp2: break else: if hbond.verbose: print("dist criteria failed (all > %g)" % sqrt(gen_rp2)) return False theta = gen_theta if hbond.verbose: print("dist criteria OK") return test_theta(dp, donor_hyds, ap, theta)
def don_upsilon_tau(donor, donor_hyds, acceptor, sp2_O_r2, sp2_O_upsilon_low, sp2_O_upsilon_high, sp2_O_theta, sp2_O_tau, sp3_O_r2, sp3_O_upsilon_low, sp3_O_upsilon_high, sp3_O_theta, sp3_O_tau, sp3_O_phi, sp3_N_r2, sp3_N_upsilon_low, sp3_N_upsilon_high, sp3_N_theta, sp3_N_tau, sp3_N_upsilon_N, gen_r2, gen_upsilon_low, gen_upsilon_high, gen_theta, tau_sym): if hbond.verbose: print("don_upsilon_tau") acc_type = acceptor.idatm_type if acc_type not in type_info: return False geom = type_info[acc_type].geometry element = acceptor.element.name if element == 'O' and geom == planar: if hbond.verbose: print("planar O") return test_upsion_tau_acceptor(donor, donor_hyds, acceptor, sp2_O_r2, sp2_O_upsilon_low, sp2_O_upsilon_high, sp2_O_theta, sp2_O_tau, tau_sym) elif element == 'O' and geom == tetrahedral or element == 'N' and geom == planar: if hbond.verbose: print("planar N or tet O") return test_upsion_tau_acceptor(donor, donor_hyds, acceptor, sp3_O_r2, sp3_O_upsilon_low, sp3_O_upsilon_high, sp3_O_theta, sp3_O_tau, tau_sym) elif element == 'N' and geom == tetrahedral: if hbond.verbose: print("tet N") # test upsilon at the N # see if lone pairs point at the donor bonded_pos = [] for bonded in acceptor.neighbors: bonded_pos.append(bonded._hb_coord) if len(bonded_pos) > 1: ap = acceptor._hb_coord dp = donor._hb_coord lone_pairs = bond_positions(ap, tetrahedral, 1.0, bonded_pos) for lp in lone_pairs: up_pos = ap - (lp - ap) ang = angle(up_pos, ap, dp) if ang >= sp3_N_upsilon_N: if hbond.verbose: print("upsilon(N) okay (%g >= %g)" % (ang, sp3_N_upsilon_N)) break else: if hbond.verbose: print("all upsilon(N) failed (< %g)" % sp3_N_upsilon_N) return False elif hbond.verbose: print("lone pair positions indeterminate at N; upsilon(N) default okay") return test_upsion_tau_acceptor(donor, donor_hyds, acceptor, sp3_N_r2, sp3_N_upsilon_low, sp3_N_upsilon_high, sp3_N_theta, sp3_N_tau, tau_sym) else: if hbond.verbose: print("generic acceptor") if acceptor.element.name == "S": gen_r2 = sulphur_compensate(gen_r2) return test_upsion_tau_acceptor(donor, donor_hyds, acceptor, gen_r2, gen_upsilon_low, gen_upsilon_high, gen_theta, None, None) if hbond.verbose: print("failed criteria") return False
def test_tau(tau, tau_sym, don_acc, dap, op): if tau is None: if hbond.verbose: print("tau test irrelevant") return True # sulfonamides and phosphonamides can have bonded NH2 groups that are planar enough # to be declared Npl, so use the hydrogen positions to determine planarity if possible if tau_sym == 4: bonded_pos = hyd_positions(don_acc) else: # since we expect tetrahedral hydrogens to be oppositely aligned from the attached # tetrahedral center, we can't use their positions for tau testing bonded_pos = [] heavys = [a for a in don_acc.neighbors if a.element.number > 1] if 2 * len(bonded_pos) != tau_sym: bonded_pos = hyd_positions(heavys[0], include_lone_pairs=True) for b in heavys[0].neighbors: if b == don_acc or b.element.number < 2: continue bonded_pos.append(b._hb_coord) if not bonded_pos: if hbond.verbose: print("tau indeterminate; default okay") return True if 2 * len(bonded_pos) != tau_sym: raise AtomTypeError( "Unexpected tau symmetry (%d, should be %d) for donor/acceptor %s" % (2 * len(bonded_pos), tau_sym, don_acc)) normal = normalize_vector(heavys[0]._hb_coord - dap) if tau < 0.0: test = lambda ang, t=tau: ang <= 0.0 - t else: test = lambda ang, t=tau: ang >= t proj_other_pos = project(op, normal, 0.0) proj_da_pos = project(dap, normal, 0.0) for bpos in bonded_pos: proj_bpos = project(bpos, normal, 0.0) ang = angle(proj_other_pos, proj_da_pos, proj_bpos) if test(ang): if tau < 0.0: if hbond.verbose: print("tau okay (%g < %g)" % (ang, -tau)) return True else: if tau > 0.0: if hbond.verbose: print("tau too small (%g < %g)" % (ang, tau)) return False if tau < 0.0: if hbond.verbose: print("all taus too big (> %g)" % -tau) return False if hbond.verbose: print("all taus acceptable (> %g)" % tau) return True
def find_hbonds(session, structures, *, inter_model=True, intra_model=True, donors=None, acceptors=None, dist_slop=0.0, angle_slop=0.0, inter_submodel=False, cache_da=False, status=True): """Hydrogen bond detection based on criteria in "Three-dimensional hydrogen-bond geometry and probability information from a crystal survey", J. Computer-Aided Molecular Design, 10 (1996), 607-622 If donors and/or acceptors are specified (as :py:class:`~chimerax.atomic.Atoms` collections or anything an Atoms collection can be constructued from), then H-bond donors/acceptors are restricted to being from those atoms. Dist/angle slop are the amount that distances/angles are allowed to exceed the values given in the above reference and still be considered hydrogen bonds. 'cache_da' allows donors/acceptors in molecules to be cached if it is anticipated that the same structures will be examined for H-bonds repeatedly (e.g. a dynamics trajectory). If 'per_coordset' is True and 'structures' contains a single structure with multiple coordinate sets, then hydrogen bonds will be computed for each coordset. If 'status' is True, progress will be logged to the status line. Returns a list of donor/acceptor pairs, unless the conditions for 'per_coordset' are satisfied, in which case a list of such lists will be returned, one per coordset. """ # hack to speed up coordinate lookup... from chimerax.atomic import Atoms, Atom if len(structures) == 1 or not inter_model or (len( set([ m if m.id is None else (m.id[0] if len(m.id) == 1 else m.id[:-1]) for m in structures ])) == 1 and not inter_submodel): Atom._hb_coord = Atom.coord else: Atom._hb_coord = Atom.scene_coord try: if donors and not isinstance(donors, Atoms): limited_donors = Atoms(donors) else: limited_donors = donors if acceptors and not isinstance(acceptors, Atoms): limited_acceptors = Atoms(acceptors) else: limited_acceptors = acceptors global _d_cache, _a_cache, _prev_limited if cache_da: if limited_donors: dIDs = [id(d) for d in limited_donors] dIDs.sort() else: dIDs = None if limited_acceptors: aIDs = [id(a) for a in limited_acceptors] aIDs.sort() else: aIDs = None key = (dIDs, aIDs) if _prev_limited and _prev_limited != key: flush_cache() _prev_limited = key from weakref import WeakKeyDictionary if _d_cache is None: _d_cache = WeakKeyDictionary() _a_cache = WeakKeyDictionary() else: flush_cache() global donor_params, acceptor_params global processed_donor_params, processed_acceptor_params global _compute_cache global verbose global _problem _problem = None global _truncated _truncated = set() bad_connectivities = 0 # Used (as necessary) to cache expensive calculations (by other functions also) _compute_cache = {} process_key = (dist_slop, angle_slop) if process_key not in processed_acceptor_params: # copy.deepcopy() refuses to copy functions (even as # references), so do this instead... a_params = [] for p in acceptor_params: a_params.append(copy.copy(p)) for i in range(len(a_params)): a_params[i][3] = _process_arg_tuple(a_params[i][3], dist_slop, angle_slop) processed_acceptor_params[process_key] = a_params else: a_params = processed_acceptor_params[process_key] # compute some info for generic acceptors/donors generic_acc_info = {} # oxygens... generic_O_acc_args = _process_arg_tuple([3.53, 90], dist_slop, angle_slop) generic_acc_info['misc_O'] = (acc_generic, generic_O_acc_args) # dictionary based on bonded atom's geometry... generic_acc_info['O2-'] = { single: (acc_generic, generic_O_acc_args), linear: (acc_generic, generic_O_acc_args), planar: (acc_phi_psi, _process_arg_tuple([3.53, 90, 130], dist_slop, angle_slop)), tetrahedral: (acc_generic, generic_O_acc_args) } generic_acc_info['O3-'] = generic_acc_info['O2-'] generic_acc_info['O2'] = { single: (acc_generic, generic_O_acc_args), linear: (acc_generic, generic_O_acc_args), planar: (acc_phi_psi, _process_arg_tuple([3.30, 110, 130], dist_slop, angle_slop)), tetrahedral: (acc_theta_tau, _process_arg_tuple([3.03, 100, -180, 145], dist_slop, angle_slop)) } # list based on number of known bonded atoms... generic_acc_info['O3'] = [(acc_generic, generic_O_acc_args), (acc_theta_tau, _process_arg_tuple([3.17, 100, -161, 145], dist_slop, angle_slop)), (acc_phi_psi, _process_arg_tuple([3.42, 120, 135], dist_slop, angle_slop))] # nitrogens... generic_N_acc_args = _process_arg_tuple([3.42, 90], dist_slop, angle_slop) generic_acc_info['misc_N'] = (acc_generic, generic_N_acc_args) generic_acc_info['N2'] = (acc_phi_psi, _process_arg_tuple([3.42, 140, 135], dist_slop, angle_slop)) # tuple based on number of bonded heavy atoms... generic_N3_mult_heavy_acc_args = _process_arg_tuple( [3.30, 153, -180, 145], dist_slop, angle_slop) generic_acc_info['N3'] = ( (acc_generic, generic_N_acc_args), # only one example to draw from; weaken by .1A, 5 degrees (acc_theta_tau, _process_arg_tuple([3.13, 98, -180, 150], dist_slop, angle_slop)), (acc_theta_tau, generic_N3_mult_heavy_acc_args), (acc_theta_tau, generic_N3_mult_heavy_acc_args)) # one example only; weaken by .1A, 5 degrees generic_acc_info['N1'] = (acc_theta_tau, _process_arg_tuple([3.40, 136, -180, 145], dist_slop, angle_slop)) # sulfurs... # one example only; weaken by .1A, 5 degrees generic_acc_info['S2'] = (acc_phi_psi, _process_arg_tuple([3.83, 85, 140], dist_slop, angle_slop)) generic_acc_info['Sar'] = generic_acc_info['S3-'] = ( acc_generic, _process_arg_tuple([3.83, 85], dist_slop, angle_slop)) # now the donors... # planar nitrogens gen_don_Npl_1h_params = (don_theta_tau, _process_arg_tuple([ 2.23, 136, 2.23, 141, 140, 2.46, 136, 140 ], dist_slop, angle_slop)) gen_don_Npl_2h_params = (don_upsilon_tau, _process_arg_tuple([ 3.30, 90, -153, 135, -45, 3.30, 90, -146, 140, -37.5, 130, 3.40, 108, -166, 125, -35, 140 ], dist_slop, angle_slop)) gen_don_O_dists = [2.41, 2.28, 2.28, 3.27, 3.14, 3.14] gen_don_O_params = (don_generic, _process_arg_tuple(gen_don_O_dists, dist_slop, angle_slop)) gen_don_N_dists = [2.36, 2.48, 2.48, 3.30, 3.42, 3.42] gen_don_N_params = (don_generic, _process_arg_tuple(gen_don_N_dists, dist_slop, angle_slop)) gen_don_S_dists = [2.42, 2.42, 2.42, 3.65, 3.65, 3.65] gen_don_S_params = (don_generic, _process_arg_tuple(gen_don_S_dists, dist_slop, angle_slop)) generic_don_info = { 'O': gen_don_O_params, 'N': gen_don_N_params, 'S': gen_don_S_params } from chimerax.atom_search import AtomSearchTree metal_coord = {} acc_trees = {} hbonds = [] has_sulfur = {} for structure in structures: if status: session.logger.status("Finding acceptors in model '%s'" % structure.name, blank_after=0) if structure.PBG_METAL_COORDINATION in structure.pbg_map: for pb in structure.pbg_map[ structure.PBG_METAL_COORDINATION].pseudobonds: a1, a2 = pb.atoms if a1.element.is_metal: metal_coord.setdefault(a2, []).append(a1) if a2.element.is_metal: metal_coord.setdefault(a1, []).append(a2) if cache_da and structure in _a_cache and ( dist_slop, angle_slop) in _a_cache[structure]: acc_atoms = [] acc_data = [] for acc_atom, data in _a_cache[structure][( dist_slop, angle_slop)].items(): if not acc_atom.deleted: acc_atoms.append(acc_atom) acc_data.append(data) else: acc_atoms, acc_data = _find_acceptors(structure, a_params, limited_acceptors, generic_acc_info) if cache_da: cache = WeakKeyDictionary() for i in range(len(acc_atoms)): cache[acc_atoms[i]] = acc_data[i] if structure not in _a_cache: _a_cache[structure] = {} _a_cache[structure][(dist_slop, angle_slop)] = cache #xyz = [] has_sulfur[structure] = False for acc_atom in acc_atoms: #c = acc_atom._hb_coord #xyz.append([c[0], c[1], c[2]]) if acc_atom.element == Element.get_element('S'): has_sulfur[structure] = True if status: session.logger.status("Building search tree of acceptor atoms", blank_after=0) acc_trees[structure] = AtomSearchTree( acc_atoms, data=acc_data, sep_val=3.0, scene_coords=(Atom._hb_coord == Atom.scene_coord)) if process_key not in processed_donor_params: # find max donor distances before they get squared.. # copy.deepcopy() refuses to copy functions (even as # references), so do this instead... d_params = [] for p in donor_params: d_params.append(copy.copy(p)) for di in range(len(d_params)): geom_type = d_params[di][2] arg_list = d_params[di][4] don_rad = Element.bond_radius('N') if geom_type == theta_tau: max_dist = max((arg_list[0], arg_list[2], arg_list[5])) elif geom_type == upsilon_tau: max_dist = max((arg_list[0], arg_list[5], arg_list[11])) elif geom_type == water: max_dist = max((arg_list[1], arg_list[4], arg_list[8])) else: max_dist = max(gen_don_O_dists + gen_don_N_dists + gen_don_S_dists) don_rad = Element.bond_radius('S') d_params[di].append(max_dist + dist_slop + don_rad + Element.bond_radius('H')) for i in range(len(d_params)): d_params[i][4] = _process_arg_tuple(d_params[i][4], dist_slop, angle_slop) processed_donor_params[process_key] = d_params else: d_params = processed_donor_params[process_key] generic_water_params = _process_arg_tuple( [2.36, 2.36 + OH_bond_dist, 146], dist_slop, angle_slop) generic_theta_tau_params = _process_arg_tuple([2.48, 132], dist_slop, angle_slop) generic_upsilon_tau_params = _process_arg_tuple([3.42, 90, -161, 125], dist_slop, angle_slop) generic_generic_params = _process_arg_tuple([2.48, 3.42, 130, 90], dist_slop, angle_slop) for dmi in range(len(structures)): structure = structures[dmi] if status: session.logger.status("Finding donors in model '%s'" % structure.name, blank_after=0) if cache_da and structure in _d_cache and ( dist_slop, angle_slop) in _d_cache[structure]: don_atoms = [] don_data = [] for don_atom, data in _d_cache[structure][( dist_slop, angle_slop)].items(): if not don_atom.deleted: don_atoms.append(don_atom) don_data.append(data) else: don_atoms, don_data = _find_donors(structure, d_params, limited_donors, generic_don_info) if cache_da: cache = WeakKeyDictionary() for i in range(len(don_atoms)): cache[don_atoms[i]] = don_data[i] if structure not in _d_cache: _d_cache[structure] = {} _d_cache[structure][(dist_slop, angle_slop)] = cache if status: session.logger.status( "Matching donors in model '%s' to acceptors" % structure.name, blank_after=0) for i in range(len(don_atoms)): donor_atom = don_atoms[i] geom_type, tau_sym, arg_list, test_dist = don_data[i] donor_hyds = hyd_positions(donor_atom) coord = donor_atom._hb_coord for acc_structure in structures: if acc_structure == structure and not intra_model or acc_structure != structure and not inter_model: continue if not inter_submodel \ and acc_structure.id and structure.id \ and acc_structure.id[0] == structure.id[0] \ and acc_structure.id[:-1] == structure.id[:-1] \ and acc_structure.id[1:] != structure.id[1:]: continue if has_sulfur[acc_structure]: from .common_geom import SULFUR_COMP td = test_dist + SULFUR_COMP else: td = test_dist accs = acc_trees[acc_structure].search(coord, td) if verbose: session.logger.info( "Found %d possible acceptors for donor %s:" % (len(accs), donor_atom)) for acc_data in accs: session.logger.info("\t%s\n" % acc_data[0]) for acc_atom, geom_func, args in accs: if acc_atom == donor_atom: # e.g. hydroxyl if verbose: print("skipping: donor == acceptor") continue try: if not geom_func(donor_atom, donor_hyds, *args): continue except ConnectivityError as e: session.logger.info( "Skipping possible acceptor with bad geometry: %s\n%s\n" % (acc_atom, e)) bad_connectivities += 1 continue except Exception: print("donor:", donor_atom, " acceptor:", acc_atom) raise if verbose: session.logger.info( "\t%s satisfies acceptor criteria" % acc_atom) if geom_type == upsilon_tau: donor_func = don_upsilon_tau add_args = generic_upsilon_tau_params + [tau_sym] elif geom_type == theta_tau: donor_func = don_theta_tau add_args = generic_theta_tau_params elif geom_type == water: donor_func = don_water add_args = generic_water_params else: if donor_atom.idatm_type in ["Npl", "N2+"]: heavys = 0 for bonded in donor_atom.neighbors: if bonded.element.number > 1: heavys += 1 if heavys > 1: info = gen_don_Npl_1h_params else: info = gen_don_Npl_2h_params else: info = generic_don_info[ donor_atom.element.name] donor_func, arg_list = info add_args = generic_generic_params if donor_func == don_upsilon_tau: # tack on generic # tau symmetry add_args = generic_upsilon_tau_params + [4] elif donor_func == don_theta_tau: add_args = generic_theta_tau_params try: if not donor_func(donor_atom, donor_hyds, acc_atom, *tuple(arg_list + add_args)): continue except ConnectivityError as e: session.logger.info( "Skipping possible donor with bad geometry: %s\n%s\n" % (donor_atom, e)) bad_connectivities += 1 continue except AtomTypeError as e: session.logger.warning(str(e)) #_problem = ("atom type", donor_atom, str(e), None) continue if verbose: session.logger.info( "\t%s satisfies donor criteria" % donor_atom) # ensure hbond isn't precluded by metal-coordination... if acc_atom in metal_coord: from chimerax.geometry import angle conflict = False for metal in metal_coord[acc_atom]: if angle(donor_atom._hb_coord, acc_atom._hb_coord, metal._hb_coord) < 90.0: if verbose: session.logger.info( "\tH-bond conflicts with" " metal coordination to %s" % metal) conflict = True break if conflict: continue hbonds.append((donor_atom, acc_atom)) if status: session.logger.status("") if bad_connectivities: session.logger.warning( "Skipped %d atom(s) with bad connectivities; see log for details" % bad_connectivities) if _problem: if session.ui.is_gui: # report a bug when atom matches multiple donor/acceptor descriptions da, atom, grp1, grp2 = _problem res_atoms = atom.residue.atoms def res_atom_rep(a): try: i = res_atoms.index(a) except ValueError: return "other %s" % a.element.name return "%2d" % (i + 1) descript = "geometry class 1: %s\n\ngeometry class 2: %s" % ( repr(grp1), repr(grp2)) from chimerax.core.logger import report_exception report_exception( error_description= """At least one atom was classified into more than one acceptor or donor geometry class. This indicates a problem in the donr/acceptor classification mechanism and we would appreciate it if you would use the bug-report button below to send us the information that will allow us to improve the classification code. residue name: %s problem %s atom: %d residue atoms: %s residue bonds: %s %s """ % (atom.residue.name, da, res_atoms.index(atom) + 1, "\n\t".join([ "%2d %-4s %-s (%s)" % (en[0] + 1, en[1].name, en[1].idatm_type, str(en[1].coord)) for en in enumerate(res_atoms) ]), "\n\t".join([ "%s <-> %-s" % (res_atom_rep(b.atoms[0]), res_atom_rep(b.atoms[1])) for b in atom.residue.atoms.bonds ]), descript)) _problem = None if _truncated: if len(_truncated) > 20: session.logger.warning( "%d atoms were skipped as donors/acceptors due to missing" " heavy-atom bond partners" % len(_truncated)) else: session.logger.warning( "The following atoms were skipped as donors/acceptors due to missing" " heavy-atom bond partners: %s" % "; ".join([str(a) for a in _truncated])) _truncated = None finally: delattr(Atom, "_hb_coord") return hbonds