def make_fmet_from_met(session, residue): err_string = "make_fmet_from_met() is only applicable to N-terminal methionine residues!" if residue.name != 'MET': raise TypeError(err_string) n = residue.find_atom('N') if n is None: raise TypeError('Missing N atom! Is your residue complete?') for nb in n.neighbors: if nb.residue != residue: raise TypeError(err_string) if nb.element.name == 'H': nb.delete() from chimerax.mmcif import find_template_residue import numpy fme = find_template_residue(session, 'FME') ref_atoms = ('C', 'CA', 'N') r_coords = numpy.array([residue.find_atom(a).coord for a in ref_atoms]) t_coords = numpy.array([fme.find_atom(a).coord for a in ref_atoms]) from chimerax.geometry import align_points tf, rms = align_points(t_coords, r_coords) from chimerax.atomic.struct_edit import add_atom atom_pairs = (('N', 'H'), ('N', 'CN'), ('CN', 'O1'), ('CN', 'HCN')) for (bname, aname) in atom_pairs: ta = fme.find_atom(aname) add_atom(aname, ta.element, residue, tf * ta.coord, bonded_to=residue.find_atom(bname)) residue.name = 'FME'
def get_ccd_template_and_name(session, tname): from ..openmm.amberff.template_utils import template_name_to_ccd_name from chimerax import mmcif ccd_name, extra_info = template_name_to_ccd_name(tname) if ccd_name is None: return (None, '') try: tmpl = mmcif.find_template_residue(session, ccd_name) except ValueError: return (None, "No CCD template found") description = tmpl.description if extra_info is not None: description += ', ' + extra_info return (tmpl, description)
def match_ff_templates_to_ccd_templates(session, forcefield, ccd_names): ''' For each template in the MD forcefield, try to find the CCD template that most closely matches it. This will take a long time! ''' from chimerax.isolde.graph import make_graph_from_residue_template from chimerax import mmcif ff_templates = forcefield._templates best_matches = {} ccd_graphs = {} small_ccd_templates = [] for name in ccd_names: ccd_template = mmcif.find_template_residue(session, name) if len(ccd_template.atoms) > 2: ccd_graphs[name] = make_graph_from_residue_template(ccd_template) else: small_ccd_templates.append(ccd_template) for tname, template in ff_templates.items(): best_score = -1000 if len(template.atoms) < 4: best_match, score = match_small_ff_template_to_ccd_templates( template, small_ccd_templates) best_matches[tname] = (best_match, score) continue tgraph = template.graph num_heavy_atoms = len(tgraph.labels[tgraph.labels != 1]) for ccd_name, ccd_graph in ccd_graphs.items(): ccd_heavy_atoms = len(ccd_graph.labels[ccd_graph.labels != 1]) if abs(ccd_heavy_atoms - num_heavy_atoms) > 2: continue fti, cti, _ = tgraph.maximum_common_subgraph(ccd_graph) score = len(fti) * 3 - len(tgraph.labels) - len(ccd_graph.labels) if score > best_score: best_matches[tname] = ([ccd_name], score) best_score = score elif score == best_score: best_matches[tname][0].append(ccd_name) print('Best matches for {}: {}'.format( tname, ', '.join(best_matches[tname][0])), flush=True) return best_matches
def fix_residue_to_match_md_template(session, residue, md_template, cif_template=None, rename=False): ''' For the given residue, add/remove atoms as necessary to match the given MD template. If no explicit cif_template argument is given, an attempt will be made to find a match using ISOLDE's database. If no CIF template is found, corrections to the residue will be limited to deleting excess atoms. ''' import numpy from chimerax.isolde.openmm.amberff.template_utils import ( template_name_to_ccd_name, match_template_atoms_to_ccd_atoms) if cif_template is None: ccd_name, _ = template_name_to_ccd_name(md_template.name) if ccd_name is not None: from chimerax import mmcif try: cif_template = mmcif.find_template_residue(session, ccd_name) except ValueError: pass else: ccd_name = cif_template.name if cif_template is None: return trim_residue_to_md_template(residue, md_template) template_indices, ccd_indices = match_template_atoms_to_ccd_atoms( session, md_template, ccd_name) fix_residue_from_template(residue, cif_template, template_indices=ccd_indices) from chimerax.atomic import Atoms ratoms = Atoms( [residue.find_atom(cif_template.atoms[i].name) for i in ccd_indices]) residue_indices = residue.atoms.indices(ratoms) #template_extra_atoms = [md_template.atoms[i] for i in template_extra_indices] add_missing_md_template_atoms(session, residue, md_template, residue_indices, template_indices)
def match_template_atoms_to_ccd_atoms(session, template, ccd_name=None, timeout=5): if ccd_name is None: ccd_name, _ = template_name_to_ccd_name(template.name) if ccd_name is None: raise TypeError('MD template {} has no match in the CCD!'.format( template.name)) matches = _template_atom_to_ccd_atom_cache.get((template.name, ccd_name)) if matches is not None: return matches from chimerax import mmcif ccd_template = mmcif.find_template_residue(session, ccd_name) if len(template.atoms) > 2: # Try by name first (typically very fast) from chimerax.isolde.graph import make_graph_from_residue_template tgraph = template.graph ccd_graph = make_graph_from_residue_template(ccd_template) ti, ci, timed_out = tgraph.maximum_common_subgraph(ccd_graph, big_first=True, timeout=timeout) if timed_out: ti_2, ci_2, to_2 = tgraph.maximum_common_subgraph(ccd_graph, timeout=timeout) if len(ti_2) > len(ti): ti = ti_2 ci = ci_2 if to_2: warn_str = ( 'Graph matching of MD template {} to CCD template {}' ' timed out. Match may not be optimal').format( template.name, ccd_name) session.logger.warning(warn_str) _template_atom_to_ccd_atom_cache[(template.name, ccd_name)] = (ti, ci) return ti, ci import numpy ti = [] ci = [] for i, a in enumerate(template.atoms): ti.append(i) candidates = [] for j, ca in enumerate(ccd_template.atoms): if j not in ci: if ca.element.number == a.element.atomic_number: candidates.append((j, ca)) if not len(candidates): raise TypeError( 'MD Template {} does not match CCD template {}!'.format( template.name, ccd_template.name)) elif len(candidates) == 1: ci.append(candidates[0][0]) else: # Check to see if we can distinguish by name name_match = False for j, ca in candidates: if ca.name == a.name: name_match = True candidates.append(j) break if not name_match: # Just pick the first ci.append(candidates[0][0]) ti = numpy.array(ti) ci = numpy.array(ci) _template_atom_to_ccd_atom_cache[(template.name, ccd_name)] = (ti, ci) return ti, ci
def replace_residue(session, residue, new_residue_name): block_if_sim_running(session) from chimerax.core.errors import UserError from chimerax.isolde.cmd import isolde_start isolde_start(session) from chimerax.atomic import Residues if isinstance(residue, Residues): if len(residue) != 1: raise UserError('Must have a single residue selected!') residue = residue[0] if len(residue.neighbors): raise UserError( 'Replacement by graph matching is currently only ' 'supported for ligands with no covalent bonds to other residues!') from ..template_utils import (fix_residue_from_template, fix_residue_to_match_md_template) from chimerax import mmcif try: cif_template = mmcif.find_template_residue(session, new_residue_name) except: err_str = ( 'Could not find a mmCIF template for residue name {}. ' 'For novel residues not in the Chemical Components Dictionary, ' 'you will need to provide this first.').format(new_residue_name) raise UserError(err_str) fix_residue_from_template(residue, cif_template, rename_residue=True, match_by='element') ff = session.isolde.forcefield_mgr[session.isolde.sim_params.forcefield] ligand_db = session.isolde.forcefield_mgr.ligand_db( session.isolde.sim_params.forcefield) from chimerax.isolde.openmm.openmm_interface import find_residue_templates from chimerax.atomic import Residues tdict = find_residue_templates(Residues([residue]), ff, ligand_db=ligand_db, logger=session.logger) md_template_name = tdict.get(0) if md_template_name is not None: fix_residue_to_match_md_template(session, residue, ff._templates[md_template_name], cif_template=cif_template) from chimerax.atomic import Atoms, Atom chiral_centers = Atoms( [a for a in residue.atoms if residue.ideal_chirality(a.name) != 'N']) if len(chiral_centers): warn_str = ( 'Rebuilt ligand {} has chiral centres at atoms {} ' '(highlighted). Since the current algorithm used to match topologies ' 'is not chirality aware, you should check these sites carefully to ' 'ensure they are sensible. If in doubt, it is best to delete with ' '"del #{}/{}:{}{}" and replace with "isolde add ligand {}".' ).format(residue.name, ','.join(chiral_centers.names), residue.structure.id_string, residue.chain_id, residue.number, residue.insertion_code, residue.name) session.selection.clear() chiral_centers.selected = True chiral_centers.draw_modes = Atom.BALL_STYLE from chimerax.isolde.view import focus_on_selection focus_on_selection(session, chiral_centers) session.logger.warning(warn_str)
def _prep_add(session, structures, unknowns_info, template, need_all=False, **prot_schemes): global _serial _serial = None atoms = [] type_info_for_atom = {} naming_schemas = {} idatm_type = {} # need this later; don't want a recomp hydrogen_totals = {} # add missing OXTs of "real" C termini; # delete hydrogens of "fake" N termini after protonation # and add a single "HN" back on, using same dihedral as preceding residue; # delete extra hydrogen of "fake" C termini after protonation logger = session.logger real_N, real_C, fake_N, fake_C = determine_termini(session, structures) logger.info("Chain-initial residues that are actual N" " termini: %s" % ", ".join([str(r) for r in real_N])) logger.info("Chain-initial residues that are not actual N" " termini: %s" % ", ".join([str(r) for r in fake_N])) logger.info("Chain-final residues that are actual C" " termini: %s" % ", ".join([str(r) for r in real_C])) logger.info("Chain-final residues that are not actual C" " termini: %s" % ", ".join([str(r) for r in fake_C])) for rc in real_C: complete_terminal_carboxylate(session, rc) # ensure that N termini are protonated as N3+ (since Npl will fail) from chimerax.atomic import Sequence for nter in real_N + fake_N: n = nter.find_atom("N") if not n: continue # if residue wasn't templated, leave atom typing alone if Sequence.protein3to1(n.residue.name) == 'X': continue if not (n.residue.name == "PRO" and n.num_bonds >= 2): n.idatm_type = "N3+" coordinations = {} for struct in structures: pbg = struct.pseudobond_group(struct.PBG_METAL_COORDINATION, create_type=None) if not pbg: continue for pb in pbg.pseudobonds: for a in pb.atoms: if not need_all and a.structure not in structures: continue if not a.element.is_metal: coordinations.setdefault(a, []).append(pb.other_atom(a)) remaining_unknowns = {} type_info_class = type_info['H'].__class__ from chimerax.atomic import Residue for struct in structures: for atom in struct.atoms: if atom.element.number == 0: res = atom.residue struct.delete_atom(atom) idatm_lookup = {} if template: template_lookup = {} from chimerax.atomic import TmplResidue get_template = TmplResidue.get_template for res in struct.residues: if get_template(res.name): continue try: exemplar = template_lookup[res.name] except KeyError: from chimerax.mmcif import find_template_residue tmpl = find_template_residue(session, res.name) if not tmpl: continue from chimerax.atomic import AtomicStructure s = AtomicStructure(session) r = exemplar = template_lookup[res.name] = s.new_residue( res.name, 'A', 1) atom_map = {} for ta in tmpl.atoms: if ta.element.number > 1: a = s.new_atom(ta.name, ta.element) a.coord = ta.coord r.add_atom(a) atom_map[ta] = a for tnb in ta.neighbors: if tnb in atom_map: s.new_bond(a, atom_map[tnb]) for a in res.atoms: ea = exemplar.find_atom(a.name) if ea: a.idatm_type = ea.idatm_type for r in template_lookup.values(): r.structure.delete() template_lookup.clear() for atom in struct.atoms: atom_type = atom.idatm_type idatm_type[atom] = atom_type if atom_type in type_info: # don't want to ask for idatm_type in middle # of hydrogen-adding loop (since that will # force a recomp), so remember here type_info_for_atom[atom] = type_info[atom_type] # if atom is in standard residue but has missing bonds to # heavy atoms, skip it instead of incorrectly protonating # (or possibly throwing an error if e.g. it's planar) # also # UNK/N residues will be missing some or all of their side-chain atoms, so # skip atoms that would otherwise be incorrectly protonated due to their # missing neighbors truncated = \ atom.is_missing_heavy_template_neighbors(no_template_okay=True) \ or \ (atom.residue.name in ["UNK", "N"] and atom.residue.polymer_type != Residue.PT_NONE and unk_atom_truncated(atom)) \ or \ (atom.residue.polymer_type == Residue.PT_NUCLEIC and atom.name == "P" and atom.num_explicit_bonds < 4) if truncated: session.logger.warning( "Not adding hydrogens to %s because it is missing heavy-atom" " bond partners" % atom) type_info_for_atom[atom] = type_info_class( 4, atom.num_bonds, atom.name) else: atoms.append(atom) # sulfonamide nitrogens coordinating a metal # get an additional hydrogen stripped if coordinations.get(atom, []) and atom.element.name == "N": if "Son" in [nb.idatm_type for nb in atom.neighbors]: orig_ti = type_info[atom_type] type_info_for_atom[atom] = orig_ti.__class__( orig_ti.geometry, orig_ti.substituents - 1, orig_ti.description) continue if atom in unknowns_info: type_info_for_atom[atom] = unknowns_info[atom] atoms.append(atom) continue remaining_unknowns.setdefault(atom.residue.name, set()).add(atom.name) # leave remaining unknown atoms alone type_info_for_atom[atom] = type_info_class(4, atom.num_bonds, atom.name) for rname, atom_names in remaining_unknowns.items(): names_text = ", ".join([nm for nm in atom_names]) atom_text, obj_text = ("atoms", "them") if len(atom_names) > 1 else ("atom", "it") logger.warning( "Unknown hybridization for %s (%s) of residue type %s;" " not adding hydrogens to %s" % (atom_text, names_text, rname, obj_text)) naming_schemas.update( determine_naming_schemas(struct, type_info_for_atom)) if need_all: from chimerax.atomic import AtomicStructure for struct in [ m for m in session.models if isinstance(m, AtomicStructure) ]: if struct in structures: continue for atom in struct.atoms: idatm_type[atom] = atom.idatm_type if atom.idatm_type in type_info: type_info_for_atom[atom] = type_info[atom.idatm_type] for atom in atoms: if atom not in type_info_for_atom: continue bonding_info = type_info_for_atom[atom] total_hydrogens = bonding_info.substituents - atom.num_bonds for bonded in atom.neighbors: if bonded.element.number == 1: total_hydrogens += 1 hydrogen_totals[atom] = total_hydrogens schemes = {} # HIS and CYS treated as 'unspecified'; use built-in typing for scheme_type, res_names, res_check, typed_atoms in [ ('his', ["HID", "HIE", "HIP"], None, []), ('asp', asp_res_names, _asp_check, asp_prot_names), ('glu', glu_res_names, _glu_check, glu_prot_names), ('lys', ["LYS", "LYN"], _lys_check, ["NZ"]), ('cys', ["CYM"], _cys_check, ["SG"]) ]: scheme = prot_schemes.get(scheme_type + '_scheme', None) if scheme is None: by_name = True scheme = {} else: by_name = False if not scheme: for s in structures: for r in s.residues: if r.name in res_names and res_check and res_check(r): if by_name: scheme[r] = r.name elif scheme_type != 'his': scheme[r] = res_names[0] # unset any explicit typing... for ta in typed_atoms: a = r.find_atom(ta) if a: a.idatm_type = None else: for r in scheme.keys(): if res_check and not res_check(r, scheme[r]): del scheme[r] schemes[scheme_type] = scheme # create dictionary keyed on histidine residue with value of another # dictionary keyed on the nitrogen atoms with boolean values: True # equals should be protonated his_Ns = {} for r, protonation in schemes["his"].items(): delta = r.find_atom("ND1") epsilon = r.find_atom("NE2") if delta is None or epsilon is None: # find the ring, etc. rings = r.structure.rings() for ring in rings: if r in rings.atoms.residues: break else: continue # find CG by locating CB-CG bond ring_bonds = ring.bonds for ra in ring.atoms: if ra.element.name != "C": continue for ba, b in zip(ra.neighbors, ra.bonds): if ba.element.name == "C" and b not in ring_bonds: break else: continue break else: continue nitrogens = [a for a in ring.atoms if a.element.name == "N"] if len(nitrogens) != 2: continue if ra in nitrogens[0].neighbors: delta, epsilon = nitrogens else: epsilon, delta = nitrogens if protonation == "HID": his_Ns.update({delta: True, epsilon: False}) elif protonation == "HIE": his_Ns.update({delta: False, epsilon: True}) elif protonation == "HIP": his_Ns.update({delta: True, epsilon: True}) else: continue for n, do_prot in his_Ns.items(): if do_prot: type_info_for_atom[n] = type_info["Npl"] n.idatm_type = idatm_type[n] = "Npl" else: type_info_for_atom[n] = type_info["N2"] n.idatm_type = idatm_type[n] = "N2" for r, protonation in schemes["asp"].items(): _handle_acid_protonation_scheme_item(r, protonation, asp_res_names, asp_prot_names, type_info, type_info_for_atom) for r, protonation in schemes["glu"].items(): _handle_acid_protonation_scheme_item(r, protonation, glu_res_names, glu_prot_names, type_info, type_info_for_atom) for r, protonation in schemes["lys"].items(): nz = r.find_atom("NZ") if protonation == "LYS": it = 'N3+' else: it = 'N3' ti = type_info[it] if nz is not None: type_info_for_atom[nz] = ti # avoid explicitly setting type if possible if nz.idatm_type != it: nz.idatm_type = it for r, protonation in schemes["cys"].items(): sg = r.find_atom("SG") if protonation == "CYS": it = 'S3' else: it = 'S3-' ti = type_info[it] if sg is not None: type_info_for_atom[sg] = ti # avoid explicitly setting type if possible if sg.idatm_type != it: sg.idatm_type = it return atoms, type_info_for_atom, naming_schemas, idatm_type, \ hydrogen_totals, his_Ns, coordinations, fake_N, fake_C
def add_amino_acid_residue(model, resname, prev_res=None, next_res=None, chain_id=None, number=None, center=None, insertion_code=' ', add_b_factor=0, occupancy=1, phi=-135, psi=135): session = model.session if (not chain_id or not number or center is None) and (not prev_res and not next_res): raise TypeError('If no anchor residues are specified, chain ID, ' 'number and center must be provided!') if prev_res and next_res: raise TypeError('Cannot specify both previous and next residues!') other_atom = None insertion_point = None import numpy if prev_res: ref_res = prev_res b_factor = prev_res.atoms[numpy.in1d( prev_res.atoms.names, ('N', 'CA', 'C', 'O', 'CB'))].bfactors.mean() + add_b_factor pri = model.residues.index(prev_res) if pri > 0 and pri < len(model.residues) - 1: insertion_point = model.residues[pri + 1] catom = prev_res.find_atom('C') for n in catom.neighbors: if n.residue != prev_res: raise TypeError( 'This residue already has another bonded to its ' 'C terminus!') if chain_id is not None: session.logger.warning( 'AddAA: chain_id argument is ignored when adding to an existing residue.' ) chain_id = prev_res.chain_id oxt = prev_res.find_atom('OXT') if oxt is not None: oxt.delete() elif next_res: ref_res = next_res b_factor = next_res.atoms[numpy.in1d( next_res.atoms.names, ('N', 'CA', 'C', 'O', 'CB'))].bfactors.mean() + add_b_factor insertion_point = next_res natom = next_res.find_atom('N') for n in natom.neighbors: if n.residue != next_res: raise TypeError( 'This residue already has another bonded to its ' 'N terminus!') if chain_id is not None: session.logger.warning( 'AddAA: chain_id argument is ignored when adding to an existing residue.' ) chain_id = next_res.chain_id for hname in ('H2', 'H3'): h = next_res.find_atom(hname) if h is not None: h.delete() h = next_res.find_atom('H1') if h is not None: h.name = 'H' if next_res.name == 'PRO': h = next_res.find_atom('H') if h: h.delete() if number is None: if prev_res: number = prev_res.number + 1 elif next_res: number = next_res.number - 1 from chimerax import mmcif tmpl = mmcif.find_template_residue(session, resname) from .place_ligand import new_residue_from_template import numpy # delete extraneous atoms r = new_residue_from_template(model, tmpl, chain_id, [0, 0, 0], number, insert_code=insertion_code, b_factor=b_factor, precedes=insertion_point) r.atoms[numpy.in1d(r.atoms.names, ['OXT', 'HXT', 'H2', 'H1', 'HN1', 'HN2'])].delete() # Translate and rotate residue to (roughly) match the desired position if not next_res and not prev_res: r.atoms.coords += numpy.array(center) - r.atoms.coords.mean(axis=0) b_factor = max(add_b_factor, 5) else: from chimerax.geometry import align_points from chimerax.atomic.struct_edit import add_bond if prev_res: correct_o_position(prev_res, psi) add_bond(r.find_atom('N'), prev_res.find_atom('C')) n_pos = _find_next_N_position(prev_res) ca_pos = _find_next_CA_position(n_pos, prev_res) c_pos = _find_next_C_position(ca_pos, n_pos, prev_res, phi) target_coords = numpy.array([n_pos, ca_pos, c_pos]) align_coords = numpy.array( [r.find_atom(a).coord for a in ['N', 'CA', 'C']]) elif next_res: add_bond(r.find_atom('C'), next_res.find_atom('N')) c_pos = _find_prev_C_position(next_res, psi) ca_pos = _find_prev_CA_position(c_pos, next_res) #o_pos = _find_prev_O_position(c_pos, next_res) n_pos = _find_prev_N_position(c_pos, ca_pos, next_res, psi) target_coords = numpy.array([c_pos, ca_pos, n_pos]) align_coords = numpy.array( [r.find_atom(a).coord for a in ['C', 'CA', 'N']]) tf = align_points(align_coords, target_coords)[0] r.atoms.coords = tf * r.atoms.coords correct_o_position(r, psi) if r.name in ('GLU', 'ASP'): fix_amino_acid_protonation_state(r) if r.name == 'PRO': r.atoms[r.atoms.names == 'H'].delete() r.atoms.bfactors = b_factor r.atoms.occupancies = occupancy from . import copy_atom_style_from copy_atom_style_from(session, r.atoms, ref_res) model.atoms.selecteds = False r.atoms.selecteds = True return r
def find_incorrect_residues(session, model, heavy_atoms_only=True): from chimerax.atomic import Residue, TmplResidue from chimerax import mmcif residues = model.residues questionable = [] for r in residues: pt = r.polymer_type tmpl = None if pt in (Residue.PT_AMINO, Residue.PT_NUCLEIC): start = True end = True if pt == Residue.PT_AMINO: fa = r.find_atom('N') la = r.find_atom('C') else: fa = r.find_atom('P') la = r.find_atom("O3'") if fa is not None: for fn in fa.neighbors: if fn.residue != r: start = False break if la is not None: for ln in la.neighbors: if ln.residue != r: end = False break try: tmpl = TmplResidue.get_template(r.name, start=start, end=end) except ValueError: tmpl = None if tmpl is None: if r.name not in _fetched_templates: session.logger.info( 'Fetching CCD definition for residue {} {}{}'.format( r.name, r.chain_id, r.number)) try: tmpl = mmcif.find_template_residue(session, r.name) _fetched_templates.add(r.name) except ValueError: session.logger.warning( 'Template {} not found in the Chemical Components Dictionary' .format(r.name)) continue if heavy_atoms_only: ra_names = set(r.atoms[r.atoms.element_names != 'H'].names) ta_names = set( [a.name for a in tmpl.atoms if a.element.name != 'H']) else: ra_names = set(r.atoms.names) ta_names = set([a.name for a in tmpl.atoms]) ra_residuals = ra_names.difference(ta_names) ta_residuals = ta_names.difference(ra_names) if len(ta_residuals): if end: if pt == Residue.PT_AMINO: print( 'C-terminal residue {} {}{}; ra_residuals: {}; ta_residuals: {}' .format(r.name, r.chain_id, r.number, ra_residuals, ta_residuals)) if pt == Residue.PT_AMINO and not len( ra_residuals) and ta_residuals == set(('OXT', )): # Dangling C-terminal peptide. Allow. continue elif pt == Residue.PT_NUCLEIC and not heavy_atoms_only and not len( ra_residuals) and ta_residuals == set(("HO5'", )): # Dangling 5' end. Allow continue if start and not heavy_atoms_only: if pt == Residue.PT_AMINO and ta_residuals == set( ('H', )) and ra_residuals == set(('H1', 'H2', 'H3')): # Dangling N-terminal peptide. Allow continue elif pt == Residue.PT_NUCLEIC and ta_residuals == set( ("HO3'", )): # Dangling 3' end. Allow. continue questionable.append(r) return questionable
def place_ligand(session, ligand_id, model=None, position=None, bfactor=None, chain=None, distance_cutoff=8.0, sim_settle=False, use_md_template=True, md_template_name=None): ''' Place a ligand at the given position, or the current centre of rotation if no :attr:`position` is specified. If the :attr:`bfactor` or :attr:`chain` arguments are not specified, the water will be assigned the missing properties based on the nearest residue within :attr:`distance_cutoff` of :attr:`position`, with the assigned B-factor being (average B-factor of nearest residue)+5. If :attr:`sim_settle` is True, a small local simulation will automatically start to settle the ligand into position. For residues of more than three atoms, if you have loaded an MD template for the residue (with matching residue name), any mis-matches between atoms in the CIF and MD templates will be automatically corrected as long as the changes only involve removing atoms from the CIF template and/or adding atoms that are directly connected to a single atom in the CIF template. The most common scenario where this arises is where the protonation state of the CIF template is different from that of the MD template. Note that at this stage no attempt is made at a preliminary fit to the density, or to avoid clashes: the ligand is simply placed at the given position with coordinates defined by the template in the Chemical Components Dictionary. Except for small, rigid ligands the use of :attr:`sim_settle`=True is inadvisable. If the ligand as placed clashes badly with the existing model, the best approach is to run the command `isolde ignore ~selAtoms` then start a simulation (which will involve *only* the new residue) to pull it into position (you may want to use pins where necessary). Once satisfied that there are no severe clashes, stop the simulation and run `isolde ~ignore` to reinstate all atoms for simulation purposes, and continue with your model building. ''' from chimerax.geometry import find_closest_points from chimerax import mmcif from chimerax.core.errors import UserError if hasattr(session, 'isolde') and session.isolde.simulation_running: raise UserError('Cannot add atoms when a simulation is running!') if model is None: if hasattr(session, 'isolde') and session.isolde.selected_model is not None: model = session.isolde.selected_model else: raise UserError( 'If model is not specified, ISOLDE must be started ' 'with a model loaded') if position is None: position = session.view.center_of_rotation matoms = model.atoms if bfactor is None or chain is None: _, _, i = find_closest_points([position], matoms.coords, distance_cutoff) if not len(i): err_str = ( 'No existing atoms found within the given distance cutoff ' 'of the target position. You may repeat with a larger cutoff or ' 'explicitly specify the B-factor and chain ID') if ligand_id == 'HOH': err_str += ( ', but keep in mind that placing waters outside of ' 'H-bonding distance to the model is generally inadvisable.' ) else: err_str += '.' raise UserError(err_str) na = matoms[i[0]] if chain is None: chain = na.residue.chain_id if bfactor is None: bfactor = na.residue.atoms.bfactors.mean() + 5 tmpl = mmcif.find_template_residue(session, ligand_id) r = new_residue_from_template(model, tmpl, chain, position, b_factor=bfactor) if use_md_template and len(r.atoms) > 3: ff = session.isolde.forcefield_mgr[ session.isolde.sim_params.forcefield] if md_template_name is None: from chimerax.isolde.openmm.amberff.template_utils import ccd_to_known_template md_template_name = ccd_to_known_template.get(ligand_id, None) if md_template_name is None: ligand_db = session.isolde.forcefield_mgr.ligand_db( session.isolde.sim_params.forcefield) from chimerax.isolde.openmm.openmm_interface import find_residue_templates from chimerax.atomic import Residues tdict = find_residue_templates(Residues([r]), ff, ligand_db=ligand_db, logger=session.logger) md_template_name = tdict.get(0) md_template = None if md_template_name is not None: md_template = ff._templates.get(md_template_name, None) if md_template is None: session.logger.warning( 'place_ligand() was called with use_md_template=True, ' 'but no suitable template was found. This command has been ignored.' ) return from ..template_utils import fix_residue_to_match_md_template fix_residue_to_match_md_template(session, r, md_template, cif_template=tmpl) matoms.selected = False r.atoms.selected = True if sim_settle: if not hasattr(session, 'isolde'): session.logger.warning( 'ISOLDE is not running. sim_settle argument ignored.') elif model != session.isolde.selected_model: session.logger.warning( "New ligand was not added to ISOLDE's " "selected model. sim_settle argument ignored.") else: # defer the simulation starting until after the new atoms have been # drawn, to make sure their styling "sticks" def do_run(*_, session=session): from chimerax.core.commands import run run(session, 'isolde sim start sel') from chimerax.core.triggerset import DEREGISTER return DEREGISTER session.triggers.add_handler('frame drawn', do_run) return r