예제 #1
0
def exercise():
    from mmtbx.ions import server as s
    import iotbx.pdb.hierarchy
    import iotbx.pdb
    from cctbx.eltbx import chemical_elements

    # Assert that valence parameters exist for all common ions with their
    # coordinating atoms
    for elem in [
            "NA", "MG", "K", "CA", "MN", "FE", "NI", "CO", "CU", "ZN", "CD"
    ]:
        params = s.get_metal_parameters(elem)
        assert (len(params.allowed_coordinating_atoms) > 0)
        assert (params.charge is not None)
        for elem2 in params.allowed_coordinating_atoms:
            atom1 = iotbx.pdb.hierarchy.atom()
            atom1.name = elem
            atom1.element = elem
            atom1.charge = "+" + str(params.charge)
            atom2 = iotbx.pdb.hierarchy.atom()
            atom2.name = elem2
            atom2.element = elem2
            atom2.charge = "{:+}".format(s.get_charge(elem2))
            assert (s.get_valence_params(atom1, atom2) != (None, None))

    # Make sure we don't crash on any ion residue names
    for elem in chemical_elements.proper_upper_list():
        s.get_element(elem)
        s.get_charge(elem)

    # Make sure we don't crash on any common residue names either
    from mmtbx import monomer_library
    from mmtbx.rotamer.rotamer_eval import mon_lib_query
    mon_lib_srv = monomer_library.server.server()
    common_residues = [
        getattr(iotbx.pdb, i) for i in dir(iotbx.pdb)
        if i.startswith("common_residue_names")
        and isinstance(getattr(iotbx.pdb, i), list)
    ]

    common_atoms = {
        "H": -1,
        "C": 4,
        "N": -3,
        "O": -2,
        "S": -2,
    }
    for common in common_residues:
        for resn in common:
            mlq = mon_lib_query(resn, mon_lib_srv)
            if mlq is None:
                continue
            for atom in mlq.atom_dict().values():
                elem, charge = s._get_charge_params(resname=resn,
                                                    element=atom.type_symbol)
                from libtbx import group_args

                class GAtom(group_args):
                    def fetch_labels(self):
                        return group_args(resname=self.resname)

                    def id_str(self):
                        return "gatom=\"{} {}\"".format(
                            self.resname, self.element)

                    def charge_as_int(self):
                        return int(self.charge)

                gatom = GAtom(
                    element=atom.type_symbol,
                    resname=resn,
                    charge="0",
                )
                get_charge_val, get_elem_val = s.get_charge(
                    gatom), s.get_element(gatom)
                if elem in common_atoms:
                    assert charge == common_atoms[elem]
                assert get_charge_val == charge
                assert get_elem_val == elem

    # And we support all waters
    for resn in iotbx.pdb.common_residue_names_water:
        assert s.get_element(resn) == "O"
        assert s.get_charge(resn) == -2

    print "OK"
예제 #2
0
def _is_favorable_halide_environment(
    chem_env, scatter_env, assume_hydrogens_all_missing=Auto,
    ):
  assume_hydrogens_all_missing = True
  atom = chem_env.atom
  binds_amide_hydrogen = False
  near_cation = False
  near_lys = False
  near_hydroxyl = False
  xyz = col(atom.xyz)
  min_distance_to_cation = None
  min_distance_to_hydroxyl = min_distance_to_cation
  for contact in chem_env.contacts:
    other = contact.atom
    resname = contact.resname()
    atom_name = contact.atom_name()
    element = contact.element
    distance = abs(contact)
    # XXX need to figure out exactly what this should be - CL has a
    # fairly large radius though (1.67A according to ener_lib.cif)
    if distance < 2.5:
      return False
    if not element in ["C", "N", "H", "O", "S"]:
      charge = server.get_charge(element)
      if charge < 0 and distance <= 3.5:
        # Nearby anion that is too close
        return False
      if charge > 0 and distance <= 3.5:
        # Nearby cation
        near_cation = True
        if min_distance_to_cation is None or \
          distance < min_distance_to_cation:
          min_distance_to_cation = distance
    # Lysine sidechains (can't determine planarity)
    elif (atom_name in ["NZ"] and #, "NE", "NH1", "NH2"] and
          resname in ["LYS"] and
          distance <= 3.5):
      near_lys = True
      if min_distance_to_cation is None or \
        distance < min_distance_to_cation:
        min_distance_to_cation = distance
    # sidechain amide groups, no hydrogens (except Arg)
    # XXX this would be more reliable if we also calculate the expected
    # hydrogen positions and use the vector method below
    elif (atom_name in ["NZ", "NH1", "NH2", "ND2", "NE2"] and
          resname in ["ARG", "ASN", "GLN"] and
          (assume_hydrogens_all_missing or resname == "ARG") and
          distance <= 3.5):
      binds_amide_hydrogen = True
      if resname == "ARG" and (
          min_distance_to_cation is None or
          distance < min_distance_to_cation):
        min_distance_to_cation = distance
    # hydroxyl groups - note that the orientation of the hydrogen is usually
    # arbitrary and we can't determine precise bonding
    elif ((atom_name in ["OG1", "OG2", "OH1"]) and
          (resname in ["SER", "THR", "TYR"]) and
          (distance <= 3.5)):
      near_hydroxyl = True
      if distance < min_distance_to_hydroxyl:
        min_distance_to_hydroxyl = distance
    # Backbone amide, implicit H
    elif atom_name in ["N"] and assume_hydrogens_all_missing:
      binds_amide_hydrogen = True
      # xyz_n = col(contact.site_cart)
      # bonded_atoms = connectivity[j_seq]
      # ca_same = c_prev = None
      # for k_seq in bonded_atoms:
      #   other2 = pdb_atoms[k_seq]
      #   if other2.name.strip().upper() in ["CA"]:
      #     ca_same = col(get_site(k_seq))
      #   elif other2.name.strip().upper() in ["C"]:
      #     c_prev = col(get_site(k_seq))
      # if ca_same is not None and c_prev is not None:
      #   xyz_cca = (ca_same + c_prev) / 2
      #   vec_ncca = xyz_n - xyz_cca
      #   # 0.86 is the backbone N-H bond distance in geostd
      #   xyz_h = xyz_n + (vec_ncca.normalize() * 0.86)
      #   vec_nh = xyz_n - xyz_h
      #   vec_nx = xyz_n - xyz
      #   angle = abs(vec_nh.angle(vec_nx, deg=True))
      #   if abs(angle - 180) <= 20:
      #     binds_amide_hydrogen = True
  # now check again for negatively charged sidechain (etc.) atoms (e.g.
  # carboxyl groups), but with some leeway if a cation is also nearby.
  # backbone carbonyl atoms are also excluded.
  for contact in chem_env.contacts:
    if contact.altloc() not in ["", "A"]:
      continue
    resname = contact.resname()
    atom_name = contact.atom_name()
    distance = abs(contact)
    if ((distance < 3.2) and
        (min_distance_to_cation is not None and
         distance < (min_distance_to_cation + 0.2)) and
        halides.is_negatively_charged_oxygen(atom_name, resname)):
      return False
  return binds_amide_hydrogen or near_cation or near_lys
예제 #3
0
def exercise () :
  from mmtbx.ions import server as s
  import iotbx.pdb.hierarchy
  import iotbx.pdb
  from cctbx.eltbx import chemical_elements

  # Assert that valence parameters exist for all common ions with their
  # coordinating atoms
  for elem in ["NA", "MG", "K", "CA", "MN", "FE", "NI", "CO", "CU", "ZN", "CD"]:
    params = s.get_metal_parameters(elem)
    assert (len(params.allowed_coordinating_atoms) > 0)
    assert (params.charge is not None)
    for elem2 in params.allowed_coordinating_atoms :
      atom1 = iotbx.pdb.hierarchy.atom()
      atom1.name = elem
      atom1.element = elem
      atom1.charge = "+" + str(params.charge)
      atom2 = iotbx.pdb.hierarchy.atom()
      atom2.name = elem2
      atom2.element = elem2
      atom2.charge = "{:+}".format(s.get_charge(elem2))
      assert (s.get_valence_params(atom1, atom2) != (None, None))

  # Make sure we don't crash on any ion residue names
  for elem in chemical_elements.proper_upper_list():
    s.get_element(elem)
    s.get_charge(elem)

  # Make sure we don't crash on any common residue names either
  from mmtbx import monomer_library
  from mmtbx.rotamer.rotamer_eval import mon_lib_query
  mon_lib_srv = monomer_library.server.server()
  common_residues = [getattr(iotbx.pdb, i) for i in dir(iotbx.pdb)
                     if i.startswith("common_residue_names") and
                     isinstance(getattr(iotbx.pdb, i), list)]

  common_atoms = {
    "H": -1,
    "C": 4,
    "N": -3,
    "O": -2,
    "S": -2,
    }
  for common in common_residues:
    for resn in common:
      mlq = mon_lib_query(resn, mon_lib_srv)
      if mlq is None:
        continue
      for atom in mlq.atom_dict().values():
        elem, charge = s._get_charge_params(resname = resn,
                                            element = atom.type_symbol)
        from libtbx import group_args
        class GAtom(group_args):
          def fetch_labels(self):
            return group_args(resname = self.resname)
          def id_str(self):
            return "gatom=\"{} {}\"".format(self.resname, self.element)
          def charge_as_int(self):
            return int(self.charge)
        gatom = GAtom(
          element = atom.type_symbol,
          resname = resn,
          charge = "0",
          )
        get_charge_val, get_elem_val = s.get_charge(gatom), s.get_element(gatom)
        if elem in common_atoms:
          assert charge == common_atoms[elem]
        assert get_charge_val == charge
        assert get_elem_val == elem

  # And we support all waters
  for resn in iotbx.pdb.common_residue_names_water:
    assert s.get_element(resn) == "O"
    assert s.get_charge(resn) == -2

  print "OK"
예제 #4
0
def is_favorable_halide_environment(i_seq,
                                    contacts,
                                    pdb_atoms,
                                    sites_frac,
                                    connectivity,
                                    unit_cell,
                                    params,
                                    assume_hydrogens_all_missing=Auto):
    """
  Detects if an atom's site exists in a favorable environment for a halide
  ion. This includes coordinating by a positively charged sidechain or backbone
  as well as an absense of negatively charged coordinating groups.

  Parameters
  ----------
  i_seq : int
  contacts : list of mmtbx.ions.environment.atom_contact
  pdb_atoms : iotbx.pdb.hierarchy.af_shared_atom
  sites_frac : tuple of float, float, float
  connectivity : scitbx.array_family.shared.stl_set_unsigned
  unit_cell : uctbx.unit_cell
  params : libtbx.phil.scope_extract
  assume_hydrogens_all_missing : bool, optional

  Returns
  -------
  bool
  """
    if (assume_hydrogens_all_missing in [None, Auto]):
        elements = pdb_atoms.extract_element()
        assume_hydrogens_all_missing = not ("H" in elements or "D" in elements)
    atom = pdb_atoms[i_seq]
    binds_amide_hydrogen = False
    near_cation = False
    near_lys = False
    near_hydroxyl = False
    xyz = col(atom.xyz)
    min_distance_to_cation = max(unit_cell.parameters()[0:3])
    min_distance_to_hydroxyl = min_distance_to_cation
    for contact in contacts:
        # to analyze local geometry, we use the target site mapped to be in the
        # same ASU as the interacting site
        def get_site(k_seq):
            return unit_cell.orthogonalize(site_frac=(contact.rt_mx *
                                                      sites_frac[k_seq]))

        other = contact.atom
        resname = contact.resname()
        atom_name = contact.atom_name()
        element = contact.element
        distance = abs(contact)
        j_seq = other.i_seq
        # XXX need to figure out exactly what this should be - CL has a
        # fairly large radius though (1.67A according to ener_lib.cif)
        if (distance < params.min_distance_to_other_sites):
            return False
        if not element in ["C", "N", "H", "O", "S"]:
            charge = server.get_charge(element)
            if charge < 0 and distance <= params.min_distance_to_anion:
                # Nearby anion that is too close
                return False
            if charge > 0 and distance <= params.max_distance_to_cation:
                # Nearby cation
                near_cation = True
                if (distance < min_distance_to_cation):
                    min_distance_to_cation = distance
        # Lysine sidechains (can't determine planarity)
        elif (atom_name in ["NZ"] and  #, "NE", "NH1", "NH2"] and
              resname in ["LYS"] and
              distance <= params.max_distance_to_cation):
            near_lys = True
            if (distance < min_distance_to_cation):
                min_distance_to_cation = distance
        # sidechain amide groups, no hydrogens (except Arg)
        # XXX this would be more reliable if we also calculate the expected
        # hydrogen positions and use the vector method below
        elif (atom_name in ["NZ", "NH1", "NH2", "ND2", "NE2"]
              and resname in ["ARG", "ASN", "GLN"]
              and (assume_hydrogens_all_missing or resname == "ARG")
              and distance <= params.max_distance_to_cation):
            if (_is_coplanar_with_sidechain(
                    atom,
                    other.parent(),
                    distance_cutoff=params.max_deviation_from_plane)):
                binds_amide_hydrogen = True
                if (resname == "ARG") and (distance < min_distance_to_cation):
                    min_distance_to_cation = distance
        # hydroxyl groups - note that the orientation of the hydrogen is usually
        # arbitrary and we can't determine precise bonding
        elif ((atom_name in ["OG1", "OG2", "OH1"])
              and (resname in ["SER", "THR", "TYR"])
              and (distance <= params.max_distance_to_hydroxyl)):
            near_hydroxyl = True
            if (distance < min_distance_to_hydroxyl):
                min_distance_to_hydroxyl = distance
        # Backbone amide, explicit H
        elif atom_name in ["H"]:
            # TODO make this more general for any amide H?
            xyz_h = col(contact.site_cart)
            bonded_atoms = connectivity[j_seq]
            if (len(bonded_atoms) != 1):
                continue
            xyz_n = col(get_site(bonded_atoms[0]))
            vec_hn = xyz_h - xyz_n
            vec_hx = xyz_h - xyz
            angle = abs(vec_hn.angle(vec_hx, deg=True))
            # If Cl, H, and N line up, Cl binds the amide group
            if abs(angle - 180) <= params.delta_amide_h_angle:
                binds_amide_hydrogen = True
            else:
                pass  #print "%s N-H-X angle: %s" % (atom.id_str(), angle)
        # Backbone amide, implicit H
        elif atom_name in ["N"] and assume_hydrogens_all_missing:
            xyz_n = col(contact.site_cart)
            bonded_atoms = connectivity[j_seq]
            ca_same = c_prev = None
            for k_seq in bonded_atoms:
                other2 = pdb_atoms[k_seq]
                if other2.name.strip().upper() in ["CA"]:
                    ca_same = col(get_site(k_seq))
                elif other2.name.strip().upper() in ["C"]:
                    c_prev = col(get_site(k_seq))
            if ca_same is not None and c_prev is not None:
                xyz_cca = (ca_same + c_prev) / 2
                vec_ncca = xyz_n - xyz_cca
                # 0.86 is the backbone N-H bond distance in geostd
                xyz_h = xyz_n + (vec_ncca.normalize() * 0.86)
                vec_nh = xyz_n - xyz_h
                vec_nx = xyz_n - xyz
                angle = abs(vec_nh.angle(vec_nx, deg=True))
                if abs(angle - 180) <= params.delta_amide_h_angle:
                    binds_amide_hydrogen = True
        # sidechain NH2 groups, explicit H
        elif ((atom_name in ["HD1", "HD2"] and resname in ["ASN"])
              or (atom_name in ["HE1", "HE2"] and resname in ["GLN"])):
            # XXX not doing this for Arg because it can't handle the bidentate
            # coordination
            #(atom_name in ["HH11","HH12","HH21","HH22"] and resname == "ARG")):
            bonded_atoms = connectivity[j_seq]
            assert (len(bonded_atoms) == 1)
            xyz_n = col(get_site(bonded_atoms[0]))
            xyz_h = col(contact.site_cart)
            vec_nh = xyz_n - xyz_h
            vec_xh = xyz - xyz_h
            angle = abs(vec_nh.angle(vec_xh, deg=True))
            if abs(angle - 180) <= params.delta_amide_h_angle:
                binds_amide_hydrogen = True
            else:
                pass  #print "%s amide angle: %s" % (atom.id_str(), angle)
    # now check again for negatively charged sidechain (etc.) atoms (e.g.
    # carboxyl groups), but with some leeway if a cation is also nearby.
    # backbone carbonyl atoms are also excluded.
    for contact in contacts:
        if (contact.altloc() not in ["", "A"]):
            continue
        resname = contact.resname()
        atom_name = contact.atom_name()
        distance = abs(contact)
        if ((distance < 3.2) and (distance < (min_distance_to_cation + 0.2))
                and is_negatively_charged_oxygen(atom_name, resname)):
            #print contact.id_str(), distance
            return False
    return (binds_amide_hydrogen or near_cation or near_lys)
예제 #5
0
def is_favorable_halide_environment (
    i_seq,
    contacts,
    pdb_atoms,
    sites_frac,
    connectivity,
    unit_cell,
    params,
    assume_hydrogens_all_missing=Auto) :
  """
  Detects if an atom's site exists in a favorable environment for a halide
  ion. This includes coordinating by a positively charged sidechain or backbone
  as well as an absense of negatively charged coordinating groups.

  Parameters
  ----------
  i_seq : int
  contacts : list of mmtbx.ions.environment.atom_contact
  pdb_atoms : iotbx.pdb.hierarchy.af_shared_atom
  sites_frac : tuple of float, float, float
  connectivity : scitbx.array_family.shared.stl_set_unsigned
  unit_cell : uctbx.unit_cell
  params : libtbx.phil.scope_extract
  assume_hydrogens_all_missing : bool, optional

  Returns
  -------
  bool
  """
  if (assume_hydrogens_all_missing in [None, Auto]) :
    elements = pdb_atoms.extract_element()
    assume_hydrogens_all_missing = not ("H" in elements or "D" in elements)
  atom = pdb_atoms[i_seq]
  binds_amide_hydrogen = False
  near_cation = False
  near_lys = False
  near_hydroxyl = False
  xyz = col(atom.xyz)
  min_distance_to_cation = max(unit_cell.parameters()[0:3])
  min_distance_to_hydroxyl = min_distance_to_cation
  for contact in contacts :
    # to analyze local geometry, we use the target site mapped to be in the
    # same ASU as the interacting site
    def get_site (k_seq) :
      return unit_cell.orthogonalize(
        site_frac = (contact.rt_mx * sites_frac[k_seq]))
    other = contact.atom
    resname = contact.resname()
    atom_name = contact.atom_name()
    element = contact.element
    distance = abs(contact)
    j_seq = other.i_seq
    # XXX need to figure out exactly what this should be - CL has a
    # fairly large radius though (1.67A according to ener_lib.cif)
    if (distance < params.min_distance_to_other_sites) :
      return False
    if not element in ["C", "N", "H", "O", "S"]:
      charge = server.get_charge(element)
      if charge < 0 and distance <= params.min_distance_to_anion:
        # Nearby anion that is too close
        return False
      if charge > 0 and distance <= params.max_distance_to_cation:
        # Nearby cation
        near_cation = True
        if (distance < min_distance_to_cation) :
          min_distance_to_cation = distance
    # Lysine sidechains (can't determine planarity)
    elif (atom_name in ["NZ"] and #, "NE", "NH1", "NH2"] and
          resname in ["LYS"] and
          distance <= params.max_distance_to_cation):
      near_lys = True
      if (distance < min_distance_to_cation) :
        min_distance_to_cation = distance
    # sidechain amide groups, no hydrogens (except Arg)
    # XXX this would be more reliable if we also calculate the expected
    # hydrogen positions and use the vector method below
    elif (atom_name in ["NZ","NH1","NH2","ND2","NE2"] and
          resname in ["ARG","ASN","GLN"] and
          (assume_hydrogens_all_missing or resname == "ARG") and
          distance <= params.max_distance_to_cation) :
      if (_is_coplanar_with_sidechain(atom, other.parent(),
            distance_cutoff = params.max_deviation_from_plane)) :
        binds_amide_hydrogen = True
        if (resname == "ARG") and (distance < min_distance_to_cation) :
          min_distance_to_cation = distance
    # hydroxyl groups - note that the orientation of the hydrogen is usually
    # arbitrary and we can't determine precise bonding
    elif ((atom_name in ["OG1", "OG2", "OH1"]) and
          (resname in ["SER", "THR", "TYR"]) and
          (distance <= params.max_distance_to_hydroxyl)) :
      near_hydroxyl = True
      if (distance < min_distance_to_hydroxyl) :
        min_distance_to_hydroxyl = distance
    # Backbone amide, explicit H
    elif atom_name in ["H"]:
      # TODO make this more general for any amide H?
      xyz_h = col(contact.site_cart)
      bonded_atoms = connectivity[j_seq]
      if (len(bonded_atoms) != 1) :
        continue
      xyz_n = col(get_site(bonded_atoms[0]))
      vec_hn = xyz_h - xyz_n
      vec_hx = xyz_h - xyz
      angle = abs(vec_hn.angle(vec_hx, deg = True))
      # If Cl, H, and N line up, Cl binds the amide group
      if abs(angle - 180) <= params.delta_amide_h_angle:
        binds_amide_hydrogen = True
      else :
        pass #print "%s N-H-X angle: %s" % (atom.id_str(), angle)
    # Backbone amide, implicit H
    elif atom_name in ["N"] and assume_hydrogens_all_missing:
      xyz_n = col(contact.site_cart)
      bonded_atoms = connectivity[j_seq]
      ca_same = c_prev = None
      for k_seq in bonded_atoms :
        other2 = pdb_atoms[k_seq]
        if other2.name.strip().upper() in ["CA"]:
          ca_same = col(get_site(k_seq))
        elif other2.name.strip().upper() in ["C"]:
          c_prev = col(get_site(k_seq))
      if ca_same is not None and c_prev is not None:
        xyz_cca = (ca_same + c_prev) / 2
        vec_ncca = xyz_n - xyz_cca
        # 0.86 is the backbone N-H bond distance in geostd
        xyz_h = xyz_n + (vec_ncca.normalize() * 0.86)
        vec_nh = xyz_n - xyz_h
        vec_nx = xyz_n - xyz
        angle = abs(vec_nh.angle(vec_nx, deg = True))
        if abs(angle - 180) <= params.delta_amide_h_angle:
          binds_amide_hydrogen = True
    # sidechain NH2 groups, explicit H
    elif ((atom_name in ["HD1","HD2"] and resname in ["ASN"]) or
          (atom_name in ["HE1","HE2"] and resname in ["GLN"])) :
          # XXX not doing this for Arg because it can't handle the bidentate
          # coordination
          #(atom_name in ["HH11","HH12","HH21","HH22"] and resname == "ARG")):
      bonded_atoms = connectivity[j_seq]
      assert (len(bonded_atoms) == 1)
      xyz_n = col(get_site(bonded_atoms[0]))
      xyz_h = col(contact.site_cart)
      vec_nh = xyz_n - xyz_h
      vec_xh = xyz - xyz_h
      angle = abs(vec_nh.angle(vec_xh, deg = True))
      if abs(angle - 180) <= params.delta_amide_h_angle:
        binds_amide_hydrogen = True
      else :
        pass #print "%s amide angle: %s" % (atom.id_str(), angle)
  # now check again for negatively charged sidechain (etc.) atoms (e.g.
  # carboxyl groups), but with some leeway if a cation is also nearby.
  # backbone carbonyl atoms are also excluded.
  for contact in contacts :
    if (contact.altloc() not in ["", "A"]) :
      continue
    resname = contact.resname()
    atom_name = contact.atom_name()
    distance = abs(contact)
    if ((distance < 3.2) and
        (distance < (min_distance_to_cation + 0.2)) and
        is_negatively_charged_oxygen(atom_name, resname)) :
      #print contact.id_str(), distance
      return False
  return (binds_amide_hydrogen or near_cation or near_lys)
예제 #6
0
def _is_favorable_halide_environment(
    chem_env,
    scatter_env,
    assume_hydrogens_all_missing=Auto,
):
    assume_hydrogens_all_missing = True
    atom = chem_env.atom
    binds_amide_hydrogen = False
    near_cation = False
    near_lys = False
    near_hydroxyl = False
    xyz = col(atom.xyz)
    min_distance_to_cation = None
    min_distance_to_hydroxyl = min_distance_to_cation
    for contact in chem_env.contacts:
        other = contact.atom
        resname = contact.resname()
        atom_name = contact.atom_name()
        element = contact.element
        distance = abs(contact)
        # XXX need to figure out exactly what this should be - CL has a
        # fairly large radius though (1.67A according to ener_lib.cif)
        if distance < 2.5:
            return False
        if not element in ["C", "N", "H", "O", "S"]:
            charge = server.get_charge(element)
            if charge < 0 and distance <= 3.5:
                # Nearby anion that is too close
                return False
            if charge > 0 and distance <= 3.5:
                # Nearby cation
                near_cation = True
                if min_distance_to_cation is None or \
                  distance < min_distance_to_cation:
                    min_distance_to_cation = distance
        # Lysine sidechains (can't determine planarity)
        elif (atom_name in ["NZ"] and  #, "NE", "NH1", "NH2"] and
              resname in ["LYS"] and distance <= 3.5):
            near_lys = True
            if min_distance_to_cation is None or \
              distance < min_distance_to_cation:
                min_distance_to_cation = distance
        # sidechain amide groups, no hydrogens (except Arg)
        # XXX this would be more reliable if we also calculate the expected
        # hydrogen positions and use the vector method below
        elif (atom_name in ["NZ", "NH1", "NH2", "ND2", "NE2"]
              and resname in ["ARG", "ASN", "GLN"]
              and (assume_hydrogens_all_missing or resname == "ARG")
              and distance <= 3.5):
            binds_amide_hydrogen = True
            if resname == "ARG" and (min_distance_to_cation is None
                                     or distance < min_distance_to_cation):
                min_distance_to_cation = distance
        # hydroxyl groups - note that the orientation of the hydrogen is usually
        # arbitrary and we can't determine precise bonding
        elif ((atom_name in ["OG1", "OG2", "OH1"])
              and (resname in ["SER", "THR", "TYR"]) and (distance <= 3.5)):
            near_hydroxyl = True
            if distance < min_distance_to_hydroxyl:
                min_distance_to_hydroxyl = distance
        # Backbone amide, implicit H
        elif atom_name in ["N"] and assume_hydrogens_all_missing:
            binds_amide_hydrogen = True
            # xyz_n = col(contact.site_cart)
            # bonded_atoms = connectivity[j_seq]
            # ca_same = c_prev = None
            # for k_seq in bonded_atoms:
            #   other2 = pdb_atoms[k_seq]
            #   if other2.name.strip().upper() in ["CA"]:
            #     ca_same = col(get_site(k_seq))
            #   elif other2.name.strip().upper() in ["C"]:
            #     c_prev = col(get_site(k_seq))
            # if ca_same is not None and c_prev is not None:
            #   xyz_cca = (ca_same + c_prev) / 2
            #   vec_ncca = xyz_n - xyz_cca
            #   # 0.86 is the backbone N-H bond distance in geostd
            #   xyz_h = xyz_n + (vec_ncca.normalize() * 0.86)
            #   vec_nh = xyz_n - xyz_h
            #   vec_nx = xyz_n - xyz
            #   angle = abs(vec_nh.angle(vec_nx, deg=True))
            #   if abs(angle - 180) <= 20:
            #     binds_amide_hydrogen = True
    # now check again for negatively charged sidechain (etc.) atoms (e.g.
    # carboxyl groups), but with some leeway if a cation is also nearby.
    # backbone carbonyl atoms are also excluded.
    for contact in chem_env.contacts:
        if contact.altloc() not in ["", "A"]:
            continue
        resname = contact.resname()
        atom_name = contact.atom_name()
        distance = abs(contact)
        if ((distance < 3.2)
                and (min_distance_to_cation is not None and distance <
                     (min_distance_to_cation + 0.2))
                and halides.is_negatively_charged_oxygen(atom_name, resname)):
            return False
    return binds_amide_hydrogen or near_cation or near_lys