Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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
Exemplo n.º 4
0
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
Exemplo n.º 5
0
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
Exemplo n.º 6
0
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
Exemplo n.º 7
0
 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
Exemplo n.º 8
0
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
Exemplo n.º 9
0
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)
Exemplo n.º 10
0
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
Exemplo n.º 11
0
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)
Exemplo n.º 12
0
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
Exemplo n.º 13
0
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
Exemplo n.º 14
0
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