def _select_sequence(objects, sequence): from chimerax.atomic import Residues, Residue sel_residues = set() base_search_string = sequence.upper() protein_search_string = base_search_string.replace('B', '[DN]').replace('Z', '[EQ]') nucleic_search_string = base_search_string.replace('R', '[AG]').replace('Y', '[CTU]').replace( 'N', '[ACGTU]') orig_res = objects.residues for chain in orig_res.chains.unique(): search_string = protein_search_string \ if chain.polymer_type == Residue.PT_PROTEIN else nucleic_search_string try: ranges = chain.search(search_string, case_sensitive=True) except ValueError as e: from chimerax.core.errors import UserError raise UserError(e) for start, length in ranges: sel_residues.update([r for r in chain.residues[start:start+length] if r]) residues = Residues(sel_residues) atoms = residues.intersect(orig_res).atoms from chimerax.core.objects import Objects fobj = Objects(atoms = atoms, bonds = atoms.intra_bonds, pseudobonds = atoms.intra_pseudobonds, models = atoms.structures.unique()) return fobj
def restore_snapshot(cls, session, data): if data['version'] != 1: raise RestoreError("unknown nucleotide session state version") nuc = _nucleotides(session) infos = data['infos'] for mol, (info, params) in infos.items(): if info is None: if not hasattr(mol, '_nucleotide_info'): continue residues = Residues(mol._nucleotide_info.keys()) residues.atoms.clear_hide_bits(HIDE_NUCLEOTIDE) _remove_nuc_drawing(nuc, mol) continue if not hasattr(mol, '_nucleotide_info'): prev_residues = None else: prev_residues = Residues(mol._nucleotide_info.keys()) nuc.structures.add(mol) nuc.need_rebuild.add(mol) _make_nuc_drawing(nuc, mol) if prev_residues is not None: mol._nucleotide_info.clear() mol._nucleotide_info.update(info) if prev_residues is not None: new_residues = Residues(info.keys()) removed_residues = prev_residues - new_residues removed_residues.atoms.clear_hide_bits(HIDE_NUCLEOTIDE) mol._ladder_params.update(params) return nuc
def sort_bound_and_unbound_ligands(residues): unbound = [] from collections import defaultdict bound_map = defaultdict(set) for r in residues: if not len(r.neighbors): unbound.append(r) cid = bound_to_polymer(r) if cid is not None: bound_map[cid].add(r) else: unbound.append(r) from chimerax.atomic import Residues return (Residues(unbound), {cid: Residues(rset) for cid, rset in bound_map.items()})
def sort_glycan_residues(residues): from chimerax.atomic import Residues polymer_stem_res = linked_polymer_residue(residues) for r in polymer_stem_res.neighbors: if r in residues: break order = [r] def _sort_walk(r, residues, order): bonds = [ r.bonds_between(n)[0] for n in r.neighbors if n in residues and n not in order ] ordered_bonds = [] for b in bonds: atoms = b.atoms if atoms[0].residue == r: ordered_bonds.append(atoms) else: ordered_bonds.append(atoms[::-1]) ordered_bonds = list( sorted(ordered_bonds, key=lambda b: (int(b[0].name[-1])))) for b in ordered_bonds: next_r = b[1].residue order.append(next_r) _sort_walk(next_r, residues, order) _sort_walk(r, residues, order) return Residues(order)
def check_if_params_exist(session, residue): from chimerax.atomic import Residues from chimerax.core.errors import UserError from chimerax.isolde.openmm.openmm_interface import ( create_openmm_topology, find_residue_templates, ) ffmgr = session.isolde.forcefield_mgr ff_name = session.isolde.sim_params.forcefield ff = ffmgr[ff_name] #ligand_db = ffmgr.ligand_db(ff_name) template_dict = find_residue_templates(Residues([residue]), ff, logger=session.logger) err_str_base = ( 'Residue name {} already maps to MD template {}. If this is a ' 'different chemical species, you will need to choose a new name.') if len(template_dict): err_str = err_str_base.format(residue.name, template_dict[0]) raise UserError(err_str) top, residue_templates = create_openmm_topology(residue.atoms, {}) for r in top.residues(): break assigned, ambiguous, unmatched = ff.assignTemplates( top, ignoreExternalBonds=False) if len(assigned): err_str = err_str_base.format(residue.name, assigned[r][0].name) raise UserError(err_str)
def sidechain_buried_score(residue): ''' Defines how "buried" a sidechain is by counting the number of heavy atoms from other residues coming within 4A of any heavy atom from the sidechain. The returned score is a value ranging from 0 to 1, where 0 indicates no contact with other atoms, and 1 indicates 3 or more other atoms per sidechain atoms. The score scales linearly with the number of contacting atoms in between these values. ''' from chimerax.geometry import find_close_points from chimerax.atomic import Residues import numpy r = residue m = r.structure other_residues = m.residues.subtract(Residues([r])) sidechain_atoms = r.atoms[numpy.logical_not( numpy.in1d(r.atoms.names, ['N', 'C', 'CA', 'O']))] if not len(sidechain_atoms): return 0 other_atoms = other_residues.atoms cp = find_close_points(sidechain_atoms.coords, other_atoms.coords, 4.0)[1] score = (len(cp) / len(sidechain_atoms)) / 3 if score > 1: score = 1 return score
def cluster_unbound_ligands(model, unbound, cutoff=5): from chimerax.geometry import find_close_points from chimerax.atomic import Residue, Residues from collections import defaultdict import numpy m = model chain_ids = m.residues.unique_chain_ids other_residues = m.residues.subtract(unbound) #polymeric = m.residues[m.residues.polymer_types!=Residue.PT_NONE] ligand_atoms = unbound.atoms[unbound.atoms.element_names != 'H'] chain_map = {} for cid in chain_ids: cres = other_residues[other_residues.chain_ids == cid] catoms = cres.atoms[cres.atoms.element_names != 'H'] ci, li = find_close_points(catoms.coords, ligand_atoms.coords, cutoff) close_ligand_atoms = ligand_atoms[li] weights = numpy.ones(len(close_ligand_atoms), numpy.double) weights[close_ligand_atoms.element_names == 'C'] = _carbon_weight weights[close_ligand_atoms.elements.is_metal] = _metal_weight chain_map[cid] = Weighted_Counter( [a.residue for a in close_ligand_atoms], weights) unclassified = [] closest_chain_map = defaultdict(list) for r in unbound: max_atoms = 0 closest = None for cid in chain_ids: close = chain_map[cid].get(r, None) if close is not None: if close > max_atoms: closest = cid max_atoms = close if closest is not None: closest_chain_map[closest].append(r) else: unclassified.append(r) return { cid: Residues(residues) for cid, residues in closest_chain_map.items() }, Residues(unclassified)
def ncs_average_map(session, asu_map, full_map, reference_chain, ncs_chains): from chimerax.match_maker.match import ( defaults, align ) grid_points = asu_map.grid_points(asu_map.model_transform().inverse()) from chimerax.atomic import Residues, Atoms interpolated_maps = [] interpolated_maps.append(asu_map.data.matrix()) original_map_position = full_map.position dssp_cache={} for ncs in ncs_chains: score, s1, s2 = align(session, ncs, reference_chain, defaults['matrix'], 'nw', defaults['gap_open'], defaults['gap_extend'], dssp_cache) ref_residues = [] ncs_residues = [] for i, (mr, rr) in enumerate(zip(s1, s2)): if mr=='.' or rr=='.': continue ref_res = s1.residues[s1.gapped_to_ungapped(i)] match_res = s2.residues[s2.gapped_to_ungapped(i)] if not ref_res or not match_res: continue ref_residues.append(ref_res) ncs_residues.append(match_res) ref_residues = Residues(ref_residues) ncs_residues = Residues(ncs_residues) (ref_pa, ncs_pa) = paired_principal_atoms([ref_residues, ncs_residues]) from chimerax.geometry import align_points tf, rms = align_points(ncs_pa.coords, ref_pa.coords) #full_map.position = tf * full_map.position from chimerax.core.commands import run #run(session, "fitmap #{} in #{}".format(full_map.id_string, asu_map.id_string)) interpolated_maps.append( full_map.interpolated_values(grid_points, point_xform=tf.inverse()).reshape(asu_map.data.size)) #full_map.position = original_map_position asu_map.data.matrix()[:] = sum(interpolated_maps)/len(interpolated_maps) asu_map.data.values_changed()
def parameterise_ligand(session, residue, net_charge=None, charge_method='am1-bcc'): from chimerax.core.errors import UserError if len(residue.neighbors) != 0: raise UserError( f"Residue is covalently bound to {len(residue.neighbors)} other residues. " "ISOLDE currenly only supports parameterising new non-covalent ligands." ) import numpy element_check = numpy.isin(residue.atoms.element_names, _supported_elements) if not numpy.all(element_check): unsupported = residue.atoms[numpy.logical_not(element_check)] raise UserError( 'Automatic ligand parameterisation currently only supports the following elements:\n' f'{", ".join(_supported_elements)}\n' f'Residue type {residue.name} contains the unsupported elements {", ".join(unsupported.element_names.unique())}.' 'If you wish to work with this residue you will need to parameterise it using an external package.' ) import os base_path = os.path.dirname(os.path.abspath(__file__)) gaff2_parms = os.path.join(base_path, 'gaff2.dat') from chimerax.add_charge.charge import nonstd_charge, estimate_net_charge from chimerax.atomic import Residues if net_charge is None: # Workaround - trigger recalculation of idatm_types if necessary to make sure rings are current atom_types = residue.atoms.idatm_types net_charge = estimate_net_charge(residue.atoms) from chimerax.amber_info import amber_bin import tempfile with tempfile.TemporaryDirectory() as tempdir: nonstd_charge(session, Residues([residue]), net_charge, charge_method, temp_dir=tempdir) ante_out = os.path.join(tempdir, "ante.out.mol2") parmchk2 = os.path.join(amber_bin, 'parmchk2') frcmod_file = os.path.join(tempdir, "residue.frcmod") import subprocess subprocess.check_output([ parmchk2, '-s', '2', '-i', ante_out, '-f', 'mol2', '-p', gaff2_parms, '-o', frcmod_file ]) from .amber_convert import amber_to_ffxml amber_to_ffxml(frcmod_file, ante_out, output_name=residue.name + '.xml')
def parameterise_cmd(session, residues, override=False, net_charge=None, always_raise_errors=True): from chimerax.core.errors import UserError unique_residue_types = [ residues[residues.names == name][0] for name in residues.unique_names ] for residue in unique_residue_types: if hasattr(session, 'isolde'): ff_name = session.isolde.sim_params.forcefield forcefield = session.isolde.forcefield_mgr[ff_name] ligand_db = session.isolde.forcefield_mgr.ligand_db(ff_name) from chimerax.isolde.openmm.openmm_interface import find_residue_templates from chimerax.atomic import Residues templates = find_residue_templates(Residues([residue]), forcefield, ligand_db=ligand_db, logger=session.logger) if len(templates): if not override: raise UserError( f'Residue name {residue.name} already corresponds to template {templates[0]} in ' f'the {ff_name} forcefield. If you wish to replace that template, re-run this ' 'command with override=True') try: parameterise_ligand(session, residue, net_charge=net_charge) except Exception as e: if always_raise_errors: raise UserError(str(e)) else: session.logger.warning( f'Parameterisation of {residue.name} failed with the following message:' ) session.logger.warning(str(e)) continue session.logger.info( f'OpenMM ffXML file {residue.name} written to the current working directory.' ) if hasattr(session, 'isolde'): if len(templates): forcefield._templates.pop(templates[0]) forcefield.loadFile(f'{residue.name}.xml', resname_prefix='USER_') session.logger.info( f'New template added to forcefield as USER_{residue.name}. This ligand should ' 'now work in all remaining simulations for this session. To use in ' 'future sessions, load the ffXML file with ISOLDE\'s Load Residue MD Definition(s) button.' )
def set_normal(residues): molecules = residues.unique_structures nuc = _nucleotides(molecules[0].session) rds = {} for mol in molecules: _make_nuc_drawing(nuc, mol) rds[mol] = mol._nucleotide_info changed = {} for r in residues: if rds[r.structure].pop(r, None) is not None: changed.setdefault(r.structure, []).append(r) nuc.need_rebuild.update(changed.keys()) import itertools Residues(itertools.chain( *changed.values())).atoms.clear_hide_bits(HIDE_NUCLEOTIDE)
def run_script(session): from chimerax.atomic import selected_residues, selected_bonds, Residues sel = selected_residues(session) if len(sel) != 2 or not all(sel.names=='CYS'): b = selected_bonds(session) if len(b) == 1: b = b[0] sel = Residues([a.residue for a in b.atoms]) if len(sel) != 2 or not all(sel.names=='CYS'): from chimerax.core.errors import UserError raise UserError('Please select exactly two cysteine residues!') from chimerax.isolde.atomic.building.build_utils import break_disulfide break_disulfide(*sel) session.logger.info('Broke disulphide bond between {}.'.format( ' and '.join(['{}{}{}'.format(c.chain_id, c.number, c.insertion_code) for c in sel]) ))
def sequence_align_all_residues(session, residues_a, residues_b): ''' Peform a sequence alignment on each pair of chains, and concatenate the aligned residues into a single "super-alignment" (a pair of Residues objects with 1:1 correspondence between residues at each index). Each :class:`Residues` object in the arguments should contain residues from a single chain, and the chains in :param:`residues_a` and `residues_b` should be matched. Arguments: * session: - the ChimeraX session object * residues_a: - a list of ChimeraX :class:`Residues` objects * residues_b: - a list of ChimeraX :class:`Residues` objects ''' from chimerax.match_maker.match import defaults, align, check_domain_matching alignments = ([], []) dssp_cache = {} for ra, rb in zip(residues_a, residues_b): uca = ra.unique_chains ucb = rb.unique_chains if not (len(uca) == 1 and len(ucb) == 1): raise TypeError( 'Each residue selection must be from a single chain!') match, ref = [ check_domain_matching([ch], dr)[0] for ch, dr in ((uca[0], ra), (ucb[0], rb)) ] score, s1, s2 = align(session, match, ref, defaults['matrix'], 'nw', defaults['gap_open'], defaults['gap_extend'], dssp_cache) for i, (rr, mr) in enumerate(zip(s1, s2)): if mr == '.' or rr == '.': continue ref_res = s1.residues[s1.gapped_to_ungapped(i)] match_res = s2.residues[s2.gapped_to_ungapped(i)] if not ref_res or not match_res: continue alignments[0].append(ref_res) alignments[1].append(match_res) from chimerax.atomic import Residues return [Residues(a) for a in alignments]
def get_protein_backbone_problems(structure, outliers_only=True): from chimerax.isolde import session_extensions as sx rmgr = sx.get_ramachandran_mgr(structure.session) residues = structure.residues if outliers_only: f = rmgr.outliers else: f = rmgr.non_favored problems = f(residues) # Also count twisted and cis-nonPro peptide bonds here cis = rmgr.cis(residues) cis_nonpro = cis[cis.names != 'PRO'] # Note: this will probably lead to double-counting since # twisted peptides will usually also be captured by # get_torsion_restraint_problems()... but ultimately I # don't think that's a huge problem. twisted = rmgr.twisted(residues) from chimerax.atomic import concatenate, Residues twisted = Residues([t[0] for t in twisted]) problems = concatenate([problems, cis_nonpro, twisted]) problem_ramas = rmgr.get_ramas(problems) return problem_ramas
def assign_bound_ligand_chain_ids_and_residue_numbers(m, bound_map): # The wwPDB steering committee has dictated that for protein-linked glycans, # the following rules apply: # - if the modelled portion of the glycan is a single residue, it should have # the same chain ID as the protein. # - if more than one residue, it should have a unique chain ID. from chimerax.atomic import Residues, Residue, concatenate import numpy for cid, bound_residues in bound_map.items(): first_ligand_number = next_available_ligand_number(m, cid) new_glycan_cid = [] assign_to_chain = [] groups = independent_groupings(bound_residues) for g in groups: if len(g) == 1: assign_to_chain.append(g) elif any(r.name in known_sugars for r in g): new_glycan_cid.append(g) else: assign_to_chain.append(g) new_glycan_cid = list( sorted(new_glycan_cid, key=lambda g: linked_polymer_residue(g).number)) assign_to_chain = list( sorted(assign_to_chain, key=lambda g: linked_polymer_residue(g).number)) for i, g in enumerate(new_glycan_cid): gid = cid + 'gl' + str(i) residues = sort_glycan_residues(g) residues.chain_ids = gid m.renumber_residues(residues, 1) if len(assign_to_chain): assign_to_chain = concatenate( [Residues(g) for g in assign_to_chain]) assign_to_chain.chain_ids = 'XXtmp' m.renumber_residues(assign_to_chain, first_ligand_number) assign_to_chain.chain_ids = cid
def restrain_small_ligands(model, distance_cutoff=4, heavy_atom_limit=3, spring_constant=5000, bond_to_carbon=False): ''' Residues with a small number of heavy atoms can be problematic in MDFF if unrestrained, since if knocked out of density they tend to simply keep going. It is best to restrain them with distance restraints to suitable surrounding atoms or, failing that, to their starting positions. Args: * model: - a :class:`chimerax.atomic.AtomicStructure` instance * distance_cutoff (default = 3.5): - radius in Angstroms to look for candidate heavy atoms for distance restraints. If no candidates are found, a position restraint will be applied instead. * heavy_atom_limit (default = 3): - Only residues with a number of heavy atoms less than or equal to `heavy_atom_limit` will be restrained * spring_constant (default = 500): - strength of each restraint, in :math:`kJ mol^{-1} nm^{-2}` * bond_to_carbon (default = `False`): - if `True`, only non-carbon heavy atoms will be restrained using distance restraints. ''' from chimerax.atomic import Residue, Residues residues = model.residues ligands = residues[residues.polymer_types == Residue.PT_NONE] small_ligands = Residues([ r for r in ligands if len(r.atoms[r.atoms.element_names != 'H']) < heavy_atom_limit ]) from .. import session_extensions as sx drm = sx.get_distance_restraint_mgr(model) prm = sx.get_position_restraint_mgr(model) all_heavy_atoms = model.atoms[model.atoms.element_names != 'H'] if not bond_to_carbon: all_heavy_atoms = all_heavy_atoms[all_heavy_atoms.element_names != 'C'] all_heavy_coords = all_heavy_atoms.coords from chimerax.geometry import find_close_points, distance for r in small_ligands: r_heavy_atoms = r.atoms[r.atoms.element_names != 'H'] if not bond_to_carbon: r_non_carbon_atoms = r_heavy_atoms[ r_heavy_atoms.element_names != 'C'] if not len(r_non_carbon_atoms): # No non-carbon heavy atoms. Apply position restraints prs = prm.get_restraints(r_heavy_atoms) prs.targets = prs.atoms.coords prs.spring_constants = spring_constant prs.enableds = True continue r_heavy_atoms = r_non_carbon_atoms r_indices = all_heavy_atoms.indices(r_heavy_atoms) r_coords = r_heavy_atoms.coords applied_drs = False for ra, ri, rc in zip(r_heavy_atoms, r_indices, r_coords): _, found_i = find_close_points([rc], all_heavy_coords, distance_cutoff) found_i = found_i[found_i != ri] num_drs = 0 for fi in found_i: if fi in r_indices: continue dr = drm.add_restraint(ra, all_heavy_atoms[fi]) dr.spring_constant = spring_constant dr.target = distance(rc, all_heavy_coords[fi]) dr.enabled = True num_drs += 1 # applied_drs = True if num_drs < 3: # Really need at least 3 distance restraints (probably 4, actually, # but we don't want to be *too* restrictive) to be stable in 3D # space. If we have fewer than that, add position restraints to be # sure. prs = prm.add_restraints(r_heavy_atoms) prs.targets = prs.atoms.coords prs.spring_constants = spring_constant prs.enableds = True
def __init__(self, session, isolde, atoms): ''' Initialise the object, including creating the splines from the current coordinates. No restraints are applied at this stage. Args: * session: - the ChimeraX master session object * isolde: - the :class:`Isolde` session * atoms: - a :class:`chimerax.AtomicStructure` instance. All atoms must be mobile, and from a single contiguous stretch of peptide chain. All unique residues in the selection will be chosen for shifting ''' if not is_continuous_protein_chain(atoms): raise TypeError( 'Selection must be atoms from a continuous peptide chain!') self.spring_constant =\ isolde.sim_params.position_restraint_spring_constant.value_in_unit( defaults.OPENMM_SPRING_UNIT ) # kJ/mol/A2 # Number of GUI update steps between each residue along the spline self.spline_steps_per_residue = 10 self._spline_step = 1 / self.spline_steps_per_residue # Current value of the spline parameter self._current_position_on_spline = 0 from chimerax.core.triggerset import TriggerSet triggers = self.triggers = TriggerSet() for t in ( 'register shift started', 'register shift finished', 'register shift released', ): triggers.add_trigger(t) self.finished = False # Trigger handler to update position along spline self._handler = None self.session = session self.isolde = isolde from .. import session_extensions as sx self._pr_mgr = sx.get_position_restraint_mgr(isolde.selected_model) self.polymer = find_polymer(atoms) residues = atoms.unique_residues from chimerax.atomic import Residues residues = self.residues = Residues( sorted(residues, key=lambda r: (r.chain_id, r.number, r.insertion_code))) self._extended_atoms = None nres = len(residues) # We need to build up the array of atoms procedurally here, since # we want to know where there's a gap in the CB positions. atoms = self._make_atom_arrays(residues) coords = [] n_atoms = self._n_atoms = Atoms(atoms[:, 0]) ncoords = n_atoms.coords ca_atoms = self._ca_atoms = Atoms(atoms[:, 1]) cacoords = ca_atoms.coords c_atoms = self._c_atoms = Atoms(atoms[:, 2]) ccoords = c_atoms.coords nocb = numpy.equal(atoms[:, 3], None) nocb_indices = numpy.argwhere(nocb).ravel() cb_indices = self._cb_indices = numpy.argwhere( numpy.invert(nocb)).ravel() cbcoords = numpy.empty([nres, 3]) cb_atoms = self._cb_atoms = Atoms(atoms[cb_indices, 3]) cbcoords[cb_indices] = cb_atoms.coords # Fill in the missing CB positions. If CB is missing we'll assume # we're dealing with a glycine and fudge it by using the HA3 # position glyres = residues[nocb_indices] glyha3 = glyres.atoms.filter(glyres.atoms.names == 'HA3') cbcoords[nocb_indices] = glyha3.coords u_vals = numpy.arange(0, nres) # Prepare the splines nspl = self.n_spline = interpolate.splprep(ncoords.transpose(), u=u_vals) caspl = self._ca_spline = interpolate.splprep(cacoords.transpose(), u=u_vals) cspl = self._c_spline = interpolate.splprep(ccoords.transpose(), u=u_vals) cbspl = self._cb_spline = interpolate.splprep(cbcoords.transpose(), u=u_vals)
def create_merged_residue(residues, base_residue=None, new_name=None): if base_residue is None: base_residue = residues[0] if base_residue not in residues: raise TypeError('base_residue must be a member of residues!') if new_name is None: new_name = base_residue.name from chimerax.atomic import Residues from chimerax.atomic.struct_edit import add_atom, add_bond residues = Residues(residues) seen = set() for r in residues: for n in r.neighbors: seen.add(n) if not all([r in seen for r in residues]): raise TypeError( 'All residues must be connected through covalent bonds!') atoms = residues.atoms other_residues = residues.subtract(Residues([base_residue])) bonds = atoms.intra_bonds external_bonds = [] for r in seen: if r not in other_residues: for n in r.neighbors: if n in residues: external_bonds.extend(r.bonds_between(n)) next_atom_number = highest_atom_number(base_residue.atoms) + 1 atom_map = {} other_atoms = other_residues.atoms # Do heavy atoms first, so we can assign hydrogen names based on them other_heavy_atoms = other_atoms[other_atoms.element_names != 'H'] for a in other_heavy_atoms: name = a.element.name + str(next_atom_number) next_atom_number += 1 atom_map[a] = add_atom(name, a.element, base_residue, a.coord, occupancy=a.occupancy, bfactor=a.bfactor) # Add the hydrogens for a, na in atom_map.items(): base_number = na.name[1:] i = 1 for n in a.neighbors: if n.element.name == 'H': name = 'H' + base_number + str(i) i += 1 add_atom(name, n.element, base_residue, n.coord, bonded_to=na, occupancy=n.occupancy, bfactor=n.bfactor) # Add all internal bonds for a, na in atom_map.items(): for n in a.neighbors: nn = atom_map.get(n, None) if nn is not None and nn not in na.neighbors: add_bond(na, nn) # Now we just need to add the bonds between base_residue and the merged portion, and any # other bonds between the merged portion and the wider model. To avoid valence errors we'll need # to first delete the existing bonds. for b in external_bonds: atoms = b.atoms if atoms[1] in atom_map.keys(): atoms = atoms[::-1] na = atom_map[atoms[0]] a = atoms[1] b.delete() add_bond(na, a) other_residues.delete() base_residue.name = new_name
def interpolate(self, m, res_interp, color_segments=False): """Interpolate to new conformation 'm'.""" # # Find matching set of residues. First try for # one-to-one residue match, and, if that fails, # then finding a common set of residues. # from . import segment sm = self.mol from time import time t0 = time() cf = self.core_fraction mhs = self.min_hinge_spacing log = self.log if self.match_same: results = segment.segmentHingeSame(sm, m, cf, mhs, log=log) else: from .segment import AtomPairingError try: results = segment.segmentHingeExact(sm, m, cf, mhs, log=log) except AtomPairingError: try: results = segment.segmentHingeApproximate(sm, m, cf, mhs, log=log) except AtomPairingError as e: from chimerax.core.errors import UserError raise UserError(str(e)) t1 = time() global ht ht += t1 - t0 segments, atomMap = results from chimerax.atomic import Residues, Atoms res_groups = [Residues(r0) for r0, r1 in segments] if len(atomMap) < sm.num_atoms: paired_atoms = Atoms(tuple(atomMap.keys())) unpaired_atoms = sm.atoms.subtract(paired_atoms) unpaired_atoms.delete() if sm.deleted: from chimerax.core.errors import UserError raise UserError('No atoms matched') # # Interpolate between current conformation in trajectory # and new conformation # t0 = time() # Make coordinate set arrays for starting and final coordinates nc = sm.coordset_size matoms = sm.atoms maindices = matoms.coord_indices from numpy import float64, empty coords0 = empty((nc, 3), float64) coords0[maindices] = matoms.coords coords1 = empty((nc, 3), float64) # Convert to trajectory local coordinates. xform = sm.scene_position.inverse() * m.scene_position coords1[maindices] = xform * Atoms([atomMap[a] for a in matoms]).coords from .interpolate import SegmentInterpolator seg_interp = SegmentInterpolator(res_groups, self.method, coords0, coords1) from .interpolate import interpolate coordsets = interpolate(coords0, coords1, seg_interp, res_interp, self.rate, self.frames, sm.session.logger) base_id = max(sm.coordset_ids) + 1 for i, cs in enumerate(coordsets): sm.add_coordset(base_id + i, cs) sm.active_coordset_id = base_id + i t1 = time() global it it += t1 - t0 return res_groups
def place_peptide(structure, sequence, phi_psis, *, position=None, rot_lib=None, chain_id=None): """ Place a peptide sequence. *structure* is an AtomicStructure to add the peptide to. *sequence* contains the sequence as a string. *phi_psis* is a list of phi/psi tuples, one per residue. *position* is either an array or sequence specifying an xyz world coordinate position or None. If None, the peptide is positioned at the center of the view. *rot_lib* is the name of the rotamer library to use to position the side chains. session.rotamers.library_names lists installed rotamer library names. If None, a default library will be used. *chain_id* is the desired chain ID for the peptide. If None, earliest alphabetical chain ID not already in use in the structure will be used (upper case taking precedence over lower case). returns a Residues collection of the added residues """ if not sequence: raise PeptideError("No sequence supplied") sequence = sequence.upper() if not sequence.isupper(): raise PeptideError("Sequence contains non-alphabetic characters") from chimerax.atomic import Sequence for c in sequence: try: r3 = Sequence.protein1to3[c] except KeyError: raise PeptideError("Unrecognized protein 1-letter code: %s" % c) if r3[-1] == 'X': raise PeptideError( "Peptide sequence cannot contain ambiguity codes (i.e. '%s')" % c) if len(sequence) != len(phi_psis): raise PeptideError("Number of phi/psis not equal to sequence length") session = structure.session open_models = session.models[:] if len(open_models) == 0: need_focus = True elif len(open_models) == 1: if open_models[0] == structure: need_focus = not structure.atoms else: need_focus = False else: need_focus = False if position is None: position = session.main_view.center_of_rotation from numpy import array position = array(position) prev = [None] * 3 pos = 1 from chimerax.atomic.struct_edit import DIST_N_C, DIST_CA_N, DIST_C_CA, DIST_C_O, \ find_pt, add_atom, add_dihedral_atom serial_number = None residues = [] prev_psi = 0 if chain_id is None: chain_id = unused_chain_id(structure) from numpy import array for c, phi_psi in zip(sequence, phi_psis): phi, psi = phi_psi while structure.find_residue(chain_id, pos): pos += 1 r = structure.new_residue(Sequence.protein1to3[c], chain_id, pos) residues.append(r) for backbone, dist, angle, dihed in [('N', DIST_N_C, 116.6, prev_psi), ('CA', DIST_CA_N, 121.9, 180.0), ('C', DIST_C_CA, 110.1, phi)]: if prev[0] is None: pt = array([0.0, 0.0, 0.0]) elif prev[1] is None: pt = array([dist, 0.0, 0.0]) elif prev[2] is None: pt = find_pt(prev[0].coord, prev[1].coord, array([0.0, 1.0, 0.0]), dist, angle, 0.0) else: pt = find_pt(prev[0].coord, prev[1].coord, prev[2].coord, dist, angle, dihed) a = add_atom(backbone, backbone[0], r, pt, serial_number=serial_number, bonded_to=prev[0]) serial_number = a.serial_number + 1 prev = [a] + prev[:2] o = add_dihedral_atom("O", "O", prev[0], prev[1], prev[2], DIST_C_O, 120.4, 180 + psi, bonded=True) prev_psi = psi # C terminus O/OXT at different angle than mainchain O structure.delete_atom(o) add_dihedral_atom("O", "O", prev[0], prev[1], prev[2], DIST_C_O, 117.0, 180 + psi, bonded=True) add_dihedral_atom("OXT", "O", prev[0], prev[1], prev[2], DIST_C_O, 117.0, psi, bonded=True) from chimerax.atomic import Residues residues = Residues(residues) from chimerax.swap_res import swap_aa # swap_aa is capable of swapping all residues in one call, but need to process one by one # since side-chain clashes are only calculated against pre-existing side chains kw = {} if rot_lib: kw['rot_lib'] = rot_lib for r in residues: swap_aa(session, [r], "same", criteria="cp", log=False, **kw) # find peptide center atoms = residues.atoms coords = atoms.coords center = coords.mean(0) correction = position - center atoms.coords = coords - correction from chimerax.std_commands.dssp import compute_ss compute_ss(session, structure) if need_focus: from chimerax.core.commands import run run(session, "view") return residues
def merge_fragment(target_model, residues, chain_id=None, renumber_from=None, anchor_n=None, anchor_c=None, transform=None): ''' Copy the atoms from a fragment into the current model, optionally reassigning chain ID and numbers. If alternate conformers are present, only the active one will be copied. * Args: - target_model: the model to copy into - residues: the residues to be copied. Isotropic B-factors will be copied, but aniso_u records will be ignored. - chain_id: if provided, all residues must be from one chain. The copied residues will be given this chain ID. - renumber_from: if provided, all residues will be renumbered with an offset of (lowest residue number - renumber_from) - anchor_n: an amino acid residue or None. If provided, the first amino acid residue in the fragment will be linked to this one. Throws an error if anchor_n has another residue linked at its C-terminus. - anchor_c: an amino acid residue or None. If provided, the last amino acid residue in the fragment will be linked to this one. Throws an error if anchor_c has another residue linked at its C-terminus. - transform: a Place or None. If provided, the atoms will be placed at the transformed coordinates. ''' from chimerax.core.errors import UserError us = residues.unique_structures if len(us) != 1: raise UserError( 'All residues to be copied must be from the same model!') if (chain_id or anchor_n or anchor_c or (renumber_from is not None)) \ and len(residues.unique_chain_ids) != 1: raise UserError( 'If reassigning chain ID, renumbering or specifying ' 'N- and/or C-terminal anchors, all residues to be copied must be ' 'from a single chain!') from chimerax.atomic import Residue, Residues, Atoms if (anchor_n or anchor_c): protein_residues = residues[residues.polymer_types == Residue.PT_AMINO] if not len(protein_residues): raise UserError( 'N- and/or C-terminal anchors were specified, but ' 'the copied selection does not contain any amino acid residues!' ) import numpy fm = us[0] m = target_model residues = Residues( sorted(residues, key=lambda r: (r.chain_id, r.number, r.insertion_code))) atoms = residues.atoms coords = atoms.coords atom_map = {} if renumber_from: offset = residues[0].number - renumber_from else: offset = 0 def new_residue_number(r): if r in residues: return r.number - offset return r.number def new_chain_id(r): if r in residues and chain_id: return chain_id return r.chain_id merged_residues = m.residues.merge(residues) merged_residues = Residues( sorted(merged_residues, key=lambda r: (new_chain_id(r), new_residue_number(r), r.insertion_code))) new_residue_mask = numpy.in1d(merged_residues, residues) new_residue_indices = numpy.argwhere(new_residue_mask).ravel() existing_residue_mask = numpy.in1d(merged_residues, m.residues) existing_residue_indices = numpy.argwhere(existing_residue_mask).ravel() insertion_point_map = {} if chain_id is not None: cids = [chain_id] else: cids = residues.unique_chain_ids for cid in cids: existing_residues = m.residues[m.residues.chain_ids == cid] if not len(existing_residues): insertion_point_map[cid] = None continue existing_residue_numbers = numpy.array( [str(r.number) + r.insertion_code for r in existing_residues]) cres = residues[residues.chain_ids == cid] new_residue_numbers = numpy.array( [str(r.number - offset) + r.insertion_code for r in cres]) duplicate_flags = numpy.in1d(new_residue_numbers, existing_residue_numbers) if numpy.any(duplicate_flags): dup_residues = cres[duplicate_flags] err_str = ( 'The requested merge could not be completed because the ' 'following residues in chain {} (after applying any renumbering) ' 'will have the same residue numbers as existing residues in ' 'the target: {}').format( cid, ', '.join( str(r.number) + r.insertion_code for r in dup_residues)) raise UserError(err_str) chain_mask = (numpy.array([new_chain_id(r) for r in merged_residues]) == cid) new_r_in_chain_mask = numpy.logical_and(chain_mask, new_residue_mask) insertion_point_map[cid] = None if numpy.any(new_r_in_chain_mask): last_new_r_index = numpy.argwhere(new_r_in_chain_mask)[-1] greater_indices = existing_residue_indices[ existing_residue_indices > last_new_r_index] if len(greater_indices): insertion_point_map[cid] = merged_residues[greater_indices[0]] current_residue = None first_index = new_residue_indices[0] if first_index > 0: prev_res = merged_residues[first_index - 1] else: prev_res = None prev_new_res = None from chimerax.atomic.struct_edit import add_bond for merged_index, r in zip(new_residue_indices, residues): if chain_id: cid = chain_id else: cid = r.chain_id precedes = insertion_point_map[cid] insertion_code = r.insertion_code if insertion_code == '': insertion_code = ' ' nr = m.new_residue(r.name, cid, r.number - offset, insert=insertion_code, precedes=precedes) nr.ribbon_hide_backbone = r.ribbon_hide_backbone nr.ribbon_display = r.ribbon_display nr.ribbon_color = r.ribbon_color nr.ss_type = r.ss_type for a in r.atoms: na = atom_map[a] = m.new_atom(a.name, a.element) na.coord = a.coord na.bfactor = a.bfactor na.aniso_u6 = a.aniso_u6 na.occupancy = a.occupancy na.draw_mode = a.draw_mode na.color = a.color na.display = a.display nr.add_atom(na) for n in a.neighbors: nn = atom_map.get(n, None) if nn is not None: add_bond(na, nn) if prev_res is not None: if (r.polymer_type == Residue.PT_AMINO and prev_res not in nr.neighbors and prev_res.chain_id == cid and prev_res.polymer_type == Residue.PT_AMINO): if precedes is not None: if precedes.chain_id == cid and precedes.polymer_type == Residue.PT_AMINO: ratoms = prev_res.atoms.merge(precedes.atoms) pc = prev_res.find_atom('C') nn = nr.find_atom('N') if precedes is not None and precedes.polymer_type == Residue.PT_AMINO and precedes.chain_id == cid: nc = nr.find_atom('C') pn = precedes.find_atom('N') prev_res = nr new_atoms = Atoms(list(atom_map.values())) if transform is not None: # Using Atoms.transform() rather than simply transforming the coords, # because this also correctly transforms any anisotropic B-factors. new_atoms.transform(transform) if anchor_n: anchor_atom = anchor_n.find_atom('C') link_atom = atom_map[protein_residues[0].find_atom('N')] _remove_excess_terminal_atoms(anchor_atom) _remove_excess_terminal_atoms(link_atom) add_bond(anchor_atom, link_atom) for r in new_atoms.unique_residues: merged_atoms = anchor_n.atoms.merge(r.atoms) if anchor_c: anchor_atom = anchor_c.find_atom('N') link_atom = atom_map[protein_residues[-1].find_atom('C')] _remove_excess_terminal_atoms(anchor_atom) _remove_excess_terminal_atoms(link_atom) add_bond(anchor_atom, link_atom) new_atoms.displays = True return new_atoms
def assign_charges(session, uncharged_residues, his_scheme): from chimerax.atomic import Atom Atom.register_attr(session, "charge", "coulombic coloring", attr_type=float) by_structure = {} for r in uncharged_residues: #if r.name == 'HIS': # r._coulombic_his_scheme = his_scheme by_structure.setdefault(r.structure, []).append(r) missing_heavies = [] extra_atoms = [] copy_needed = {} for struct, residue_list in list(by_structure.items()): from chimerax.atomic import Residues by_structure[struct] = residues = Residues(residue_list) missing, extra = check_residues(residues) heavies = [info for info in missing if not info[1].startswith('H')] missing_heavies.extend(heavies) copy_needed[struct] = len(heavies) < len(missing) extra_atoms.extend(extra) if extra_atoms: from chimerax.core.commands import commas if len(extra_atoms) <= 7: atoms_text = commas([str(a) for a in extra_atoms], conjunction="and") else: atoms_text = commas([str(a) for a in extra_atoms[:5]] + ["%d other atoms" % (len(extra_atoms) - 5)], conjunction="and") if len([a for a in extra_atoms if a.element.number == 1]) == len(extra_atoms): hint = " Try deleting all hydrogens first." else: hint = "" raise ChargeError( "Atoms with non-standard names found in standard residues: %s.%s" % (atoms_text, hint)) if missing_heavies: from chimerax.core.commands import commas if len(missing_heavies) <= 7: atoms_text = commas( [str(r) + ' ' + an for r, an in missing_heavies], conjunction="and") else: atoms_text = commas( [str(r) + ' ' + an for r, an in missing_heavies[:5]] + ["%d other atoms" % (len(missing_heavies) - 5)], conjunction="and") session.logger.warning( "The following heavy (non-hydrogen) atoms are missing, which may result" " in inaccurate electrostatics: %s" % atoms_text) for struct, struct_residues in by_structure.items(): if copy_needed[struct]: session.logger.status("Copying %s" % struct, secondary=True) charged_struct = struct.copy(name="copy of " + struct.name) orig_a_to_copy = {} copy_a_to_orig = {} for o_a, c_a in zip(struct.atoms, charged_struct.atoms): orig_a_to_copy[o_a] = c_a copy_a_to_orig[c_a] = o_a orig_r_to_copy = {} copy_r_to_orig = {} for o_r, c_r in zip(struct.residues, charged_struct.residues): orig_r_to_copy[o_r] = c_r copy_r_to_orig[c_r] = o_r from chimerax.addh.cmd import cmd_addh hbond = False if his_scheme is None: if len(struct_residues[struct_residues.names == "HIS"]) > 0: hbond = True from chimerax.atomic import AtomicStructures addh_structures = AtomicStructures([charged_struct]) session.logger.status("Adding hydrogens to copy of %s" % struct, secondary=True) session.silent = True try: cmd_addh(session, addh_structures, hbond=hbond) finally: session.silent = False charged_residues = [orig_r_to_copy[r] for r in struct_residues] session.logger.status("Assigning charges to copy of %s" % struct, secondary=True) else: charged_struct = struct charged_residues = struct_residues # assign charges assign_residue_charges(charged_residues, his_scheme) if copy_needed[struct]: session.logger.status("Copying charges back to %s" % struct, secondary=True) for o_r in struct_residues: for o_a in o_r.atoms: c_a = orig_a_to_copy[o_a] for nb in c_a.neighbors: if nb.residue == c_a.residue and nb not in copy_a_to_orig: c_a.charge += nb.charge o_a.charge = c_a.charge session.logger.status("Destroying copy of %s" % struct, secondary=True) charged_struct.delete()
def _update_unparameterised_residues_list(self, *_, ff=None, ambiguous=None, unmatched=None, residues=None): table = self._residue_table tlist = self._template_list if not table.isVisible(): return table.setRowCount(0) tlist.clear() if ambiguous is None and unmatched is None: if self.isolde.selected_model is None: return residues = self.isolde.selected_model.residues h = residues.atoms[residues.atoms.element_names == 'H'] addh = False from ..dialog import choice_warning if not len(h): addh = choice_warning( 'This model does not appear to have hydrogens. Would you like to add them first?' ) elif self.suspiciously_low_h(residues): addh = choice_warning( 'This model has significantly fewer hydrogens than expected for a natural molecule. Would you like to run AddH first?' ) elif self.waters_without_h(residues): addh = choice_warning( 'Some or all waters are missing hydrogens. Would you like to add them first?' ) if addh: from chimerax.core.commands import run run(self.session, 'addh') from chimerax.atomic import Residues residues = Residues( sorted(residues, key=lambda r: (r.chain_id, r.number, r.insertion_code))) if ff is None: ffmgr = self.isolde.forcefield_mgr ff = ffmgr[self.isolde.sim_params.forcefield] ligand_db = ffmgr.ligand_db(self.isolde.sim_params.forcefield) from ..openmm.openmm_interface import find_residue_templates, create_openmm_topology template_dict = find_residue_templates(residues, ff, ligand_db=ligand_db, logger=self.session.logger) top, residue_templates = create_openmm_topology( residues.atoms, template_dict) _, ambiguous, unmatched = ff.assignTemplates( top, ignoreExternalBonds=True, explicit_templates=residue_templates) from Qt.QtWidgets import QTableWidgetItem table.setRowCount(len(unmatched) + len(ambiguous)) count = 0 for r in unmatched: by_name, by_comp = ff.find_possible_templates(r) cx_res = residues[r.index] data = ( cx_res.chain_id, cx_res.name + ' ' + str(cx_res.number), ) for j, d in enumerate(data): item = QTableWidgetItem(d) item.setData(USER_ROLE, (cx_res, by_name, by_comp)) table.setItem(count, j, item) count += 1 for r, template_info in ambiguous.items(): cx_res = residues[r.index] data = (cx_res.chain_id, cx_res.name + ' ' + str(cx_res.number), ', '.join([ti[0].name for ti in template_info])) for j, d in enumerate(data): item = QTableWidgetItem(d) item.setData(USER_ROLE, (cx_res, [], [[ti[0].name, 0] for ti in template_info])) table.setItem(count, j, item) count += 1 table.resizeColumnsToContents()
def _rebuild_molecule(trigger_name, mol): if trigger_name == 'changes': mol, changes = mol # check changes for reasons we're interested in # ie., add/delete/moving atoms if changes.num_deleted_atoms(): pass # rebuild elif not set(changes.residue_reasons()).isdisjoint(_ResidueReasons): pass # rebuild elif 'active_coordset changed' in changes.structure_reasons(): pass # rebuild else: reasons = set(changes.atom_reasons()) if reasons.isdisjoint(_AtomReasons): # no reason to rebuild return mol.update_graphics_if_needed() # need to recompute ribbon first nuc = _nucleotides(mol.session) nd = _make_nuc_drawing(nuc, mol, recreate=True) if nd is None: nuc.need_rebuild.discard(mol) return nuc_info = mol._nucleotide_info all_shapes = [] # figure out which residues are of which type because # ladder needs knowledge about the whole structure sides = {} for k in SideOptions: sides[k] = [] for r in tuple(nuc_info): if r.deleted: # Sometimes the residues are gone, # but there's a still reference to them. del nuc_info[r] continue sides[nuc_info[r]['side']].append(r) if not nuc_info: # no residues to track in structure _remove_nuc_drawing(nuc, mol) return all_residues = Residues(nuc_info.keys()) # create shapes hide_riboses = [] hide_bases = [] show_glys = [] residues = sides['ladder'] if residues: residues = Residues(residues) # redo all ladder nodes # TODO: hide hydrogen bonds between matched bases shapes, hide_residues = make_ladder(nd, residues, mol._ladder_params) all_shapes.extend(shapes) hide_riboses.extend(hide_residues) hide_bases.extend(hide_residues) residues = sides['fill/slab'] + sides['slab'] if residues: shapes, hide_residues = make_slab(nd, residues, nuc_info) all_shapes.extend(shapes) hide_bases.extend(hide_residues) show_glys.extend(hide_residues) residues = sides['tube/slab'] if residues: shapes, hide_residues = make_slab(nd, residues, nuc_info) all_shapes.extend(shapes) hide_bases.extend(hide_residues) shapes, hide_residues, need_glys = make_tube(nd, hide_residues, nuc_info) all_shapes.extend(shapes) hide_riboses.extend(hide_residues) show_glys.extend(need_glys) residues = sides['orient'] if residues: for r in residues: shapes = draw_orientation(nd, r) all_shapes.extend(shapes) hide_riboses = Residues(hide_riboses) hide_bases = Residues(hide_bases) if all_shapes: nd.add_shapes(all_shapes) if hide_bases: # Until we have equivalent of ribbon_coord for atoms # hidden by nucleotide representations, we hide the # hydrogen bonds to atoms hidden by nucleotides. hide_hydrogen_bonds(hide_bases) # make sure ribose/base atoms are hidden/shown hide_all = hide_riboses & hide_bases hide_riboses = hide_riboses - hide_all hide_bases = hide_bases - hide_all all_residues.atoms.clear_hide_bits(HIDE_NUCLEOTIDE) set_hide_atoms(BaseAtomsRE, hide_bases) rib_res = hide_all.filter(hide_all.ribbon_displays) other_res = hide_all - rib_res set_hide_atoms(BaseRiboseAtomsRE, rib_res) set_hide_atoms(BaseRiboseAtomsNoRibRE, other_res) rib_res = hide_riboses.filter(hide_riboses.ribbon_displays) other_res = hide_riboses - rib_res set_hide_atoms(RiboseAtomsRE, rib_res) set_hide_atoms(RiboseAtomsNoRibRE, other_res) for residue in show_glys: rd = nuc_info[residue] tag = standard_bases[rd['name']]['tag'] ba = residue.find_atom(anchor(BASE, tag)) c1p = residue.find_atom("C1'") if c1p and ba: c1p.clear_hide_bits(HIDE_NUCLEOTIDE) ba.clear_hide_bits(HIDE_NUCLEOTIDE) # TODO: If a hidden atom is pseudobonded to another atom, # then hide the pseudobond. nuc.need_rebuild.discard(mol)
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 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