class SchemesManager(ProviderManager): """Manager for html schemes used by all bundles""" def __init__(self, session): # Just for good form. Base class currently has no __init__. super().__init__() self.schemes = set() from chimerax.core.triggerset import TriggerSet self.triggers = TriggerSet() self.triggers.add_trigger("html schemes changed") def add_provider(self, bundle_info, name, **kw): self.schemes.add(name) def is_true(value): return value and value.casefold() in ('true', '1', 'on') from PyQt5.QtWebEngineCore import QWebEngineUrlScheme scheme = QWebEngineUrlScheme(name.encode('utf-8')) port = kw.get('defaultPort', None) if port is not None: scheme.setDefaultPort(int(port)) syntax = kw.get('syntax', None) if syntax == "Path": scheme.setSyntax(QWebEngineUrlScheme.Syntax.Path) elif syntax == "Host": scheme.setSyntax(QWebEngineUrlScheme.Syntax.Host) elif syntax == "HostAndPort": scheme.setSyntax(QWebEngineUrlScheme.Syntax.HostAndPort) elif syntax == "HostPortAndUserInformation": scheme.setSyntax( QWebEngineUrlScheme.Syntax.HostPortAndUserInformation) flags = 0 if is_true(kw.get("SecureScheme", None)): flags |= QWebEngineUrlScheme.SecureScheme if is_true(kw.get("LocalScheme", None)): flags |= QWebEngineUrlScheme.LocalScheme if is_true(kw.get("LocalAccessAllowed", None)): flags |= QWebEngineUrlScheme.LocalAccessAllowed if is_true(kw.get("NoAccessAllowed", None)): flags |= QWebEngineUrlScheme.NoAccessAllowed if is_true(kw.get("ServiceWorkersAllowed", None)): flags |= QWebEngineUrlScheme.ServiceWorkersAllowed if is_true(kw.get("ViewSourceAllowed", None)): flags |= QWebEngineUrlScheme.ViewSourceAllowed if is_true(kw.get("ContentSecurityPolicyIgnored", None)): flags |= QWebEngineUrlScheme.ContentSecurityPolicyIgnored if flags: scheme.setFlags(flags) QWebEngineUrlScheme.registerScheme(scheme) def end_providers(self): self.triggers.activate_trigger("html schemes changed", self)
class _RotamerStateManager(StateManager): def __init__(self, session, base_residue, rotamers): self.init_state_manager(session, "residue rotamers") self.session = session self.base_residue = base_residue self.rotamers = list( rotamers) # don't want auto-shrinking of a Collection self.group = session.models.add_group( rotamers, name="%s rotamers" % base_residue.string(omit_structure=True), parent=base_residue.structure) from chimerax.atomic import get_triggers self.handler = get_triggers().add_handler('changes', self._changes_cb) from chimerax.core.triggerset import TriggerSet self.triggers = TriggerSet() self.triggers.add_trigger('fewer rotamers') # but not zero self.triggers.add_trigger('self destroyed') def destroy(self): self.handler.remove() if self.group.id is not None: self.session.models.close([self.group]) self.group = self.base_residue = self.rotamers = self.session = None super().destroy() def reset_state(self, session): self.triggers.activate_trigger('self destroyed', self) self.destroy() @classmethod def restore_snapshot(cls, session, data): return cls(session, data['base residue'], data['rotamers']) def take_snapshot(self, session, flags): data = {'base residue': self.base_residue, 'rotamers': self.rotamers} return data def _changes_cb(self, trigger_name, changes): if changes.num_deleted_residues() == 0: return remaining = [rot for rot in self.rotamers if not rot.deleted] if self.base_residue.deleted: self.triggers.activate_trigger('self destroyed', self) self.destroy() return remaining = [rot for rot in self.rotamers if not rot.deleted] if len(remaining) < len(self.rotamers): if remaining: self.rotamers = remaining self.triggers.activate_trigger('fewer rotamers', self) else: self.triggers.activate_trigger('self destroyed', self) self.destroy()
class AlignSeqMenuButton(ItemMenuButton): def __init__(self, alignment, **kw): self.alignment = alignment self.triggers = TriggerSet() self.triggers.add_trigger("seqs changed") alignment.add_observer(self) super().__init__(list_func=lambda aln=alignment: alignment.seqs, key_func=lambda seq, aln=alignment: aln.seqs.index(seq), item_text_func=lambda seq: seq.name, trigger_info=[ (self.triggers, "seqs changed"), ], **kw) def destroy(self): self.alignment.remove_observer(self) super().destroy() def alignment_notification(self, note_name, note_data): if note_name == "add or remove seqs": self.triggers.activate_trigger("seqs changed", note_data)
class AlignmentsManager(StateManager, ProviderManager): """Manager for sequence alignments""" def __init__(self, session, bundle_info): # Just for good form. Neither base class currently defines __init__. super().__init__() self._alignments = {} # bundle_info needed for session save self.bundle_info = bundle_info self.session = session self.viewer_info = {'alignment': {}, 'sequence': {}} self.viewer_to_subcommand = {} from chimerax.core.triggerset import TriggerSet self.triggers = TriggerSet() self.triggers.add_trigger("new alignment") self.triggers.add_trigger("destroy alignment") self._installed_headers = {} self._installed_viewers = {} def add_provider(self, bundle_info, name, *, type=None, synonyms=[], subcommand_name=None, sequence_viewer=True, alignment_viewer=True, **kw): """Register an alignment header, or an alignment/sequence viewer and its associated subcommand. Common Parameters ---------- name : str Header or viewer name. type : str "header" or "viewer" Header Parameters ---------- (none) 'run_provider' for headers should return the header's class object. Viewer Parameters ---------- sequence_viewer : bool Can this viewer show single sequences alignment_viewer : bool Can this viewer show sequence alignments synonyms : list of str Shorthands that the user could type instead of standard_name to refer to your tool in commands. Example: ['sv'] subcommand_name : str If the viewer can be controlled by a subcommand of the 'sequence' command, the subcommand word to use after 'sequence'. 'run_provider' for viewers should will receive an 'alignment' keyword argument with the alignment to view and should return the viewer instance. """ if type == "header": self._installed_headers[name] = bundle_info elif type == "viewer": if subcommand_name: if subcommand_name in _builtin_subcommands: raise ValueError("Viewer subcommand '%s' is already a builtin" " 'sequence' subcommand name" % subcommand_name) if subcommand_name in _viewer_subcommands: raise ValueError("Viewer subcommand name '%s' is already taken" % subcommand_name) _viewer_subcommands.add(subcommand_name) self.viewer_to_subcommand[name] = subcommand_name if _commands_registered: _register_viewer_subcommand(self.session.logger, subcommand_name) if synonyms: # comma-separated text -> list synonyms = [x.strip() for x in synonyms.split(',')] if sequence_viewer: self.viewer_info['sequence'][name] = synonyms if alignment_viewer: self.viewer_info['alignment'][name] = synonyms self._installed_viewers[name] = bundle_info elif type is None: raise ValueError("Provider failed to specify type to alignments manager") else: raise ValueError("Alignments manager does not handle provider type '%s'" % type) @property def alignments(self): return list(self._alignments.values()) @property def alignments_map(self): return {k:v for k,v in self._alignments.items()} def destroy_alignment(self, alignment): if alignment.ident is not False: del self._alignments[alignment.ident] self.triggers.activate_trigger("destroy alignment", alignment) alignment._destroy() def header(self, name): if name in self._installed_headers: bundle_info = self._installed_headers[name] return bundle_info.run_provider(self.session, name, self) # not installed #TODO def headers(self, *, installed_only=True): hdrs = [] if installed_only: for name, info in self._installed_headers.items(): hdrs.append(self.header(name)) else: for name in self.header_names(installed_only=False): hdrs.append(self.header(name)) return hdrs def header_names(self, *, installed_only=True): names = [] if installed_only: for name, info in self._installed_headers.items(): bundle_info = info names.append(name) else: #TODO pass return names def new_alignment(self, seqs, identify_as, attrs=None, markups=None, auto_destroy=None, align_viewer=None, seq_viewer=None, auto_associate=True, name=None, intrinsic=False, **kw): """Create new alignment from 'seqs' Parameters ---------- seqs : list of :py:class:`~chimerax.atomic.Sequence` instances Contents of alignment identify_as : a text string (or None or False) used to identify the alignment in commands. If the string is already in use by another alignment, that alignment will be destroyed and replaced. If identify_as is None, then a unique identifer will be generated and used. The string cannot contain the ':' character, since that is used to indicate sequences within the alignment to commands. Any such characters will be replaced with '/'. If False, then a "private" alignment will be returned that will not be shown in a viewer nor affected by any commands. auto_destroy : boolean or None Whether to automatically destroy the alignment when the last viewer for it is closed. If None, then treated as False if the value of the 'viewer' keyword results in no viewer being launched, else True. align_viewer/seq_viewer : str, False or None What alignment/sequence viewer to launch. If False, do not launch a viewer. If None, use the current preference setting for the user. The string must either be the viewer's tool display_name or a synonym registered by the viewer (during its register_viewer call). auto_associate : boolean or None Whether to automatically associate structures with the alignment. A value of None is the same as False except that any StructureSeqs in the alignment will be associated with their structures. name : string or None Descriptive name of the alignment to use in viewer titles and so forth. If not provided, same as identify_as. intrinsic : boolean If True, then the alignment is treated as "coupled" to the structures associated with it in that if all associations are removed then the alignment is destroyed. Returns the created Alignment """ if self.session.ui.is_gui and identify_as is not False: if len(seqs) > 1: viewer_text = align_viewer attr = 'align_viewer' type_text = "alignment" else: viewer_text = seq_viewer attr = 'seq_viewer' type_text = "sequence" if viewer_text is None: from .settings import settings viewer_text = getattr(settings, attr).lower() if viewer_text: viewer_text = viewer_text.lower() for name, syms in self.viewer_info[type_text].items(): if name == viewer_text: viewer_name = name break if viewer_text in syms: viewer_name = name break else: self.session.logger.warning("No registered %s viewer corresponds to '%s'" % (type_text, viewer_text)) viewer_text = False else: viewer_text = False if auto_destroy is None and viewer_text: auto_destroy = True from .alignment import Alignment if identify_as is None: i = 1 while str(i) in self._alignments: i += 1 identify_as = str(i) elif identify_as is not False and ':' in identify_as: self.session.logger.info( "Illegal ':' character in alignment identifier replaced with '/'") identify_as = identify_as.replace(':', '/') if identify_as in self._alignments: self.session.logger.info( "Destroying pre-existing alignment with identifier %s" % identify_as) self.destroy_alignment(self._alignments[identify_as]) if name is None: from chimerax.atomic import StructureSeq if len(seqs) == 1 and isinstance(seqs[0], StructureSeq): sseq = seqs[0] if sseq.description: description = "%s (%s)" % (sseq.description, sseq.full_name) else: description = sseq.full_name else: description = identify_as elif identify_as is False: description = "private" else: description = name if identify_as: self.session.logger.info("Alignment identifier is %s" % identify_as) alignment = Alignment(self.session, seqs, identify_as, attrs, markups, auto_destroy, auto_associate, description, intrinsic, **kw) if identify_as: self._alignments[identify_as] = alignment if viewer_text: self._installed_viewers[viewer_name].run_provider(self.session, viewer_name, self, alignment=alignment) self.triggers.activate_trigger("new alignment", alignment) return alignment @property def registered_viewers(self, seq_or_align): """Return the registered viewers of type 'seq_or_align' (which must be "sequence" or "alignent") The return value is a list of tool names. """ return list(self.viewer_info[seq_or_align].keys()) def reset_state(self, session): for alignment in self._alignments.values(): alignment._destroy() self._alignments.clear() @staticmethod def restore_snapshot(session, data): mgr = session.alignments mgr._ses_restore(data) return mgr def take_snapshot(self, session, flags): # viewer_info is "session independent" return { 'version': 1, 'alignments': self._alignments, } def _ses_restore(self, data): for am in self._alignments.values(): am.close() self._alignments = data['alignments']
class ZoneMgr: def __init__(self, session, grid_step, radius, atoms=None, transforms=None, transform_indices=None, coords=None, pad=None, interpolation_threshold=0.75): if pad is None: pad = radius self.structure = None self._symmetry_map = {} self._atoms = atoms if atoms is not None: self.structure = self._unique_structure(atoms) self._structure_change_handler = None self.session = session self._step = grid_step self._radius = radius self._coords = coords self._pad = pad self.threshold = interpolation_threshold self._mask = None self._update_needed = False self._resize_box = True from chimerax.core.triggerset import TriggerSet self.triggers = TriggerSet() self.triggers.add_trigger('atom coords updated') @property def coords(self): if self._atoms is not None: if not len(self._symmetry_map): return self._atoms.coords else: import numpy coords = numpy.concatenate([ tf * (atoms.coords) for atoms, tf in self._symmetry_map.values() ]) # print('Transformed coords: {}'.format(coords)') return coords return self._coords def block_remask_on_coord_updates(self): if not self.triggers.is_trigger_blocked('atom coords updated'): self.triggers.manual_block('atom coords updated') def allow_remask_on_coord_updates(self): if self.triggers.is_trigger_blocked('atom coords updated'): self.triggers.manual_release('atom coords updated') @coords.setter def coords(self, coords): self._symmetry_map.clear() self.stop_tracking_changes() if self.coords is not None: prev_coord_len = len(self.coords) else: prev_coord_len = None self._atoms = None self.structure = None self._coords = coords if len(coords) == 1 and prev_coord_len == 1: self.update_needed(resize_box=False) else: self.update_needed(resize_box=True) # Clear all handlers self.triggers.delete_trigger('atom coords updated') self.triggers.add_trigger('atom coords updated') @property def atoms(self): return self._atoms @atoms.setter def atoms(self, atoms): self._coords = None self._atoms = atoms self._symmetry_map.clear() self.stop_tracking_changes() self.structure = self._unique_structure(atoms) self.start_tracking_changes() self._transforms = None self._transform_indices = None self.start_tracking_changes() self.update_needed(resize_box=True) @property def symmetry_map(self): return self._symmetry_map def set_symmetry_map(self, atoms, transforms, transform_indices): self._coords = None self._atoms = atoms import numpy unique_indices = numpy.unique(transform_indices) self._symmetry_map.clear() for i in unique_indices: mask = (transform_indices == i) self._symmetry_map[i] = (atoms[mask], transforms[i]) self.stop_tracking_changes() self.structure = self._unique_structure(atoms) self.start_tracking_changes() self.update_needed(resize_box=True) @property def radius(self): return self._radius @radius.setter def radius(self, radius): self._radius = radius self.update_needed(resize_box=True) @property def grid_step(self): return self._step @grid_step.setter def grid_step(self, step): self._step = step self.update_needed(resize_box=True) @property def pad(self): return self._pad @pad.setter def pad(self, pad): self._pad = pad self.update_needed(resize_box=True) def update_needed(self, resize_box=False): self._update_needed = True if not self._resize_box: self._resize_box = resize_box @property def mask(self): if self._mask is None: coords = self.coords if coords is None: raise TypeError('Must provide either atoms or coordinates!') self._mask = VolumeMask(self.session, coords, self.grid_step, self.radius, pad=self.pad) return self._mask def _update_mask(self): # from time import time # start_time = time() update_origin = (len(self.coords) == 1) self.mask.generate_mask(self.coords, self.radius, reuse_existing=not self._resize_box, update_origin=update_origin, step=self.grid_step, pad=self.pad) self._update_needed = False self._resize_box = False # print('Updating mask took {} ms.'.format((time()-start_time)*1000)) def get_vertex_mask(self, vertices): if self._update_needed: self._update_mask() return (self.mask.interpolated_values(vertices) >= self.threshold) def _unique_structure(self, atoms): us = atoms.unique_structures if len(us) != 1: raise TypeError( 'All atoms for zone mask must be from a single model!') return us[0] def start_tracking_changes(self): if self.structure is None or self._atoms is None: raise RuntimeError( 'Live zone mask updating is only valid for atoms!') self._structure_change_handler = self.structure.triggers.add_handler( 'changes', self._model_changes_cb) def stop_tracking_changes(self): if self._structure_change_handler is not None: if self.structure is not None: self.structure.triggers.remove_handler( self._structure_change_handler) self._structure_change_handler = None def _model_changes_cb(self, trigger_name, changes): if self._atoms is None: self._structure_change_handler = None from chimerax.core.triggerset import DEREGISTER return DEREGISTER if 'coord changed' in changes[1].atom_reasons(): self.update_needed() self.triggers.activate_trigger('atom coords updated', None)
class FileReaderManager(ProviderManager): """keeps track of frequency files that have been opened""" # XML_TAG ChimeraX :: Manager :: filereader_manager def __init__(self, session, *args, **kwargs): params = signature(super().__init__).parameters if any("name" in param for param in params): super().__init__(*args, **kwargs) else: super().__init__() self.triggers = TriggerSet() self.triggers.add_trigger(FILEREADER_CHANGE) self.triggers.add_trigger(FILEREADER_ADDED) self.triggers.add_trigger(FILEREADER_REMOVED) self.triggers.add_trigger(ADD_FILEREADER) session.triggers.add_handler(REMOVE_MODELS, self.remove_models) session.triggers.add_handler(ADD_MODELS, self.apply_preset) session.triggers.add_handler(ADD_MODELS, self.trigger_fr_add) self.triggers.add_handler(ADD_FILEREADER, self.add_filereader) #list of models with an associated FileReader object self.models = [] self.filereaders = [] self.waiting_models = [] self.waiting_filereaders = [] def trigger_fr_add(self, trigger_name, models): """FILEREADER_ADDED should not get triggered until the model is loaded""" filereaders = [] for fr, mdl in zip(self.waiting_filereaders, self.waiting_models): if mdl in models: filereaders.append(fr) if len(filereaders) > 0: self.triggers.activate_trigger(FILEREADER_ADDED, filereaders) def apply_preset(self, trigger_name, models): """if a graphical preset is set in SEQCROW settings, apply that preset to models""" for model in models: if model in self.models: if model.session.ui.is_gui: apply_seqcrow_preset(model) apply_non_seqcrow_preset(model) def add_filereader(self, trigger_name, models_and_filereaders): """add models with filereader data to our list""" models, filereaders = models_and_filereaders wait = False for model, filereader in zip(models, filereaders): self.models.append(model) self.filereaders.append(filereader) if model.atomspec == '#': wait = True self.waiting_models.append(model) self.waiting_filereaders.append(filereader) if not wait: self.triggers.activate_trigger(FILEREADER_ADDED, filereaders) self.triggers.activate_trigger(FILEREADER_CHANGE, filereaders) def remove_filereader(self, trigger_name, models_and_filereaders): models, filereaders = models_and_filereaders for model, filereader in zip(models, filereaders): self.models.remove(model) self.filereaders.remove(filereader) self.triggers.activate_trigger(FILEREADER_CHANGE, filereaders) self.triggers.activate_trigger(FILEREADER_REMOVED, filereaders) def remove_models(self, trigger_name, models): """remove models with filereader data from our list when they are closed""" removed_frs = [] for model in models: while model in self.models: ndx = self.models.index(model) removed_frs.append(self.filereaders.pop(ndx)) self.models.remove(model) if len(removed_frs) > 0: self.triggers.activate_trigger(FILEREADER_REMOVED, removed_frs) def add_provider(self, bundle_info, name, **kw): #*buzz lightyear* ah yes, the models are models self.models = self.models def get_model(self, fr): dict = self.filereader_dict for mdl in dict: if fr in dict[mdl]: return mdl def list(self, other=None): if other is None: return [fr for fr in self.filereaders] else: return [ fr for fr in self.filereaders if all(x in fr.other for x in other) ] @property def frequency_models(self): """returns a list of models with frequency data""" return [ model for model in self.filereader_dict.keys() if any( 'frequency' in fr.other for fr in self.filereader_dict[model]) ] @property def energy_models(self): """returns a list of models with frequency data""" return [ model for model in self.filereader_dict.keys() if any('energy' in fr.other for fr in self.filereader_dict[model]) ] @property def filereader_dict(self): """returns a dictionary with atomic structures:FileReader pairs""" out = {} for mdl in self.models: out[mdl] = [] for i, fr in enumerate(self.filereaders): if self.models[i] is mdl: out[mdl].append(fr) return out
class RotamerLibManager(ProviderManager): """Manager for rotmer libraries""" def __init__(self, session): self.session = session self.rot_libs = None from chimerax.core.triggerset import TriggerSet self.triggers = TriggerSet() self.triggers.add_trigger("rotamer libs changed") self._library_info = {} self.settings = _RotamerManagerSettings(session, "rotamer lib manager") self._uninstalled_suffix = " [not installed]" def library(self, name): try: lib_info = self._library_info[name] except KeyError: raise NoRotamerLibraryError("No rotamer library named %s" % name) from . import RotamerLibrary if not isinstance(lib_info, RotamerLibrary): self._library_info[name] = lib_info = lib_info.run_provider( self.session, name, self) return lib_info def library_names(self, *, installed_only=False): if not installed_only: return list(self._library_info.keys()) from . import RotamerLibrary lib_names = [] for name, info in self._library_info.items(): if isinstance(info, RotamerLibrary) or info.installed: lib_names.append(name) return lib_names def library_name_menu(self, *, initial_lib=None, installed_only=False, callback=None): from PyQt5.QtWidgets import QPushButton, QMenu menu_button = QPushButton() if initial_lib is None: lib_name = self.settings.gui_lib_name else: lib_name = initial_lib if lib_name not in self.library_names(installed_only=installed_only): lib_name = self.default_command_library_name menu_button.setText(lib_name) menu = QMenu() menu_button.setMenu(menu) menu.aboutToShow.connect(lambda menu=menu, installed=installed_only: self._menu_show_cb(menu, installed)) menu.triggered.connect(lambda action, button=menu_button, cb=callback: self._menu_choose_cb(action, button, cb)) return menu_button def library_name_option(self, *, installed_only=False): from chimerax.ui.options import Option class RotLibOption(Option): def _make_widget(self, *, mgr=self, installed_only=installed_only, **kw): self.widget = mgr.library_name_menu( initial_lib=self.default, installed_only=installed_only, callback=self.make_callback) def get_value(self): return self.widget.text() def set_value(self, val): self.widget.setText(val) value = property(get_value, set_value) def set_multiple(self): self.widget.setText(self.multiple_value) return RotLibOption @property def default_command_library_name(self): available_libs = self.library_names() for lib_name in available_libs: if "Dunbrack" in lib_name: lib = lib_name break else: if available_libs: lib = list(available_libs)[0] else: raise LimitationError("No rotamer libraries installed") return lib def add_provider(self, bundle_info, name, **kw): self._library_info[name] = bundle_info def end_providers(self): self.triggers.activate_trigger("rotamer libs changed", self) def _menu_choose_cb(self, action, button, callback): menu_text = action.text() if menu_text.endswith(self._uninstalled_suffix): lib_name = menu_text[:-len(self._uninstalled_suffix)] else: lib_name = menu_text button.setText(lib_name) self.settings.gui_lib_name = lib_name if callback: callback() def _menu_show_cb(self, menu, installed_only): menu.clear() names = self.library_names(installed_only=installed_only) if not names: raise LimitationError( "No rotamer libraries %s!" % ("installed" if installed_only else "available")) names.sort() installed = set(self.library_names(installed_only=True)) for name in names: if name in installed: menu.addAction(name) else: menu.addAction(name + self._uninstalled_suffix)
class JobManager(ProviderManager): def __init__(self, session, *args, **kwargs): # 1.2 adds an __init__ to ProviderManager that uses the manager name for things # this is not the case in 1.1 self.session = session self.local_jobs = [] self.remote_jobs = [] self.unknown_status_jobs = [] self.paused = False self._thread = None self.initialized = False self.queue_dict = {} self.formats = {} self.triggers = TriggerSet() self.triggers.add_trigger(JOB_FINISHED) self.triggers.add_handler(JOB_FINISHED, self.job_finished) self.triggers.add_trigger(JOB_STARTED) self.triggers.add_handler(JOB_STARTED, self.job_started) self.triggers.add_trigger(JOB_QUEUED) self.triggers.add_handler(JOB_QUEUED, self.check_queue) self.triggers.add_handler(JOB_QUEUED, self.write_json) params = signature(super().__init__).parameters if any("name" in param for param in params): super().__init__(*args, **kwargs) else: super().__init__() def __setattr__(self, attr, val): if attr == "paused": if val: print("paused SEQCROW queue") else: print("resumed SEQCROW queue") super().__setattr__(attr, val) @property def jobs(self): return self.local_jobs + self.remote_jobs @property def has_job_running(self): return any([job.isRunning() for job in self.local_jobs]) def add_provider(self, bundle_info, name): print("adding %s format" % name) if name in self.formats: self.session.logger.warning( "local job type %s from %s supplanted that from %s" % (name, bundle_info.name, self.formats[name].name)) self.formats[name] = bundle_info def init_queue(self): """reads cached job list to fill in the queue""" scr_dir = os.path.abspath( self.session.seqcrow_settings.settings.SCRATCH_DIR) self.jobs_list_filename = os.path.join(scr_dir, "job_list-3.json") if os.path.exists(self.jobs_list_filename): with open(self.jobs_list_filename, 'r') as f: queue_dict = load(f, cls=ATDecoder) for section in ["check", "queued", "finished", "error", "killed"]: if section not in queue_dict: continue for job in queue_dict[section]: if job['server'] == 'local': for name, bi in self.formats.items(): if name == job["format"]: job_cls = bi.run_provider( self.session, name, self) local_job = job_cls( job['name'], self.session, job['theory'], geometry=job['geometry'], auto_update=job['auto_update'], auto_open=job['auto_open'], ) if section == "check": if 'output' in job and os.path.exists( job['output']): fr = FileReader(job['output'], just_geom=False) if 'finished' in fr.other and fr.other[ 'finished']: local_job.isFinished = lambda *args, **kwargs: True else: local_job.isRunning = lambda *args, **kwargs: True self.unknown_status_jobs.append( local_job) elif section == "finished": #shh it's finished local_job.isFinished = lambda *args, **kwargs: True local_job.output_name = job['output'] local_job.scratch_dir = job['scratch'] elif section == "error": #shh it's finished local_job.isFinished = lambda *args, **kwargs: True local_job.error = True local_job.output_name = job['output'] local_job.scratch_dir = job['scratch'] elif section == "killed": local_job.isFinished = lambda *args, **kwargs: True local_job.killed = True local_job.output_name = job['output'] local_job.scratch_dir = job['scratch'] local_job.output_name = job['output'] local_job.scratch_dir = job['scratch'] self.local_jobs.append(local_job) self.session.logger.info( "added %s (%s job) from previous session" % (job['name'], job['format'])) break else: self.session.logger.warning( "local job provider for %s jobs is no longer installed," % job["format"] + "job named '%s' will be removed from the queue" % job["name"]) self.local_jobs.append(local_job) self.paused = queue_dict['job_running'] if len(queue_dict['queued']) > 0: self.check_queue() if self.paused: self.session.logger.warning( "SEQCROW's queue has been paused because a local job was running when ChimeraX was closed. The queue can be resumed with SEQCROW's job manager tool" ) self.initialized = True def write_json(self, *args, **kwargs): """updates the list of cached jobs""" d = { 'finished': [], 'queued': [], 'check': [], 'error': [], 'killed': [] } job_running = False for job in self.jobs: if not job.killed: if not job.isFinished() and not job.isRunning(): d['queued'].append(job.get_json()) elif job.isFinished() and not job.error: d['finished'].append(job.get_json()) elif job.isFinished() and job.error: d['error'].append(job.get_json()) elif job.isRunning(): d['check'].append(job.get_json()) job_running = True elif job.isFinished(): d['killed'].append(job.get_json()) d['job_running'] = job_running if not self.initialized: self.init_queue() #check if SEQCROW scratch directory exists before trying to write json if not os.path.exists(os.path.dirname(self.jobs_list_filename)): os.makedirs(os.path.dirname(self.jobs_list_filename)) with open(self.jobs_list_filename, 'w') as f: dump(d, f, cls=ATEncoder, indent=4) def job_finished(self, trigger_name, job): """when a job is finished, open or update the structure as requested""" if self.session.seqcrow_settings.settings.JOB_FINISHED_NOTIFICATION == \ 'log and popup notifications' and self.session.ui.is_gui: #it's just an error message for now #TODO: make my own logger self.session.logger.error("%s: %s" % (trigger_name, job)) else: job.session.logger.info("%s: %s" % (trigger_name, job)) if isinstance(job, LocalJob): self._thread = None if not hasattr(job, "output_name") or \ not os.path.exists(job.output_name): job.error = True else: fr = FileReader(job.output_name, just_geom=False) #XXX: finished is not added to the FileReader for ORCA and Psi4 when finished = False if 'finished' not in fr.other or not fr.other['finished']: job.error = True if job.auto_update and ( job.theory.geometry.chix_atomicstructure is not None and not job.theory.geometry.chix_atomicstructure.deleted): if os.path.exists(job.output_name): finfo = job.output_name try: finfo = (job.output_name, job.format_name, None) fr = FileReader(finfo, get_all=True, just_geom=False) if len(fr.atoms) > 0: job.session.filereader_manager.triggers.activate_trigger( ADD_FILEREADER, ([job.theory.geometry.chix_atomicstructure], [fr])) rescol = ResidueCollection(fr) rescol.update_chix( job.theory.geometry.chix_atomicstructure) except: job.update_structure() if fr.all_geom is not None and len(fr.all_geom) > 1: coordsets = rescol.all_geom_coordsets(fr) job.theory.geometry.chix_atomicstructure.remove_coordsets() job.theory.geometry.chix_atomicstructure.add_coordsets( coordsets) for i, coordset in enumerate(coordsets): job.theory.geometry.chix_atomicstructure.active_coordset_id = i + 1 for atom, coord in zip( job.theory.geometry.chix_atomicstructure.atoms, coordset): atom.coord = coord job.theory.geometry.chix_atomicstructure.active_coordset_id = job.theory.geometry.chix_atomicstructure.num_coordsets elif job.auto_open or job.auto_update: if hasattr(job, "output_name") and os.path.exists(job.output_name): if job.format_name: run( job.session, "open \"%s\" coordsets true format %s" % (job.output_name, job.format_name)) else: run(job.session, "open \"%s\" coordsets true" % job.output_name) else: self.session.logger.error("could not open output of %s" % repr(job)) self.triggers.activate_trigger(JOB_QUEUED, trigger_name) pass def job_started(self, trigger_name, job): """prints 'job started' notification to log""" job.session.logger.info("%s: %s" % (trigger_name, job)) pass def add_job(self, job): """add job (LocalJob instance) to the queue""" if not self.initialized: self.init_queue() if isinstance(job, LocalJob): self.local_jobs.append(job) self.triggers.activate_trigger(JOB_QUEUED, job) def increase_priotity(self, job): """move job (LocalJob) up one position in the queue""" if isinstance(job, LocalJob): ndx = self.local_jobs.index(job) if ndx != 0: self.local_jobs.remove(job) new_ndx = 0 for i in range(min(ndx - 1, len(self.local_jobs) - 1), -1, -1): if not self.local_jobs[i].killed and \ not self.local_jobs[i].isFinished() and \ not self.local_jobs[i].isRunning(): new_ndx = i break self.local_jobs.insert(new_ndx, job) self.triggers.activate_trigger(JOB_QUEUED, job) def decrease_priotity(self, job): """move job (LocalJob) down one position in the queue""" if isinstance(job, LocalJob): ndx = self.local_jobs.index(job) if ndx != (len(self.local_jobs) - 1): self.local_jobs.remove(job) new_ndx = len(self.local_jobs) for i in range(ndx, len(self.local_jobs)): if not self.local_jobs[i].killed and \ not self.local_jobs[i].isFinished() and \ not self.local_jobs[i].isRunning(): new_ndx = i + 1 break self.local_jobs.insert(new_ndx, job) self.triggers.activate_trigger(JOB_QUEUED, job) def check_queue(self, *args): """check to see if a waiting job can run""" for job in self.unknown_status_jobs: if isinstance(job, LocalJob): fr = FileReader(job.output_name, just_geom=False) if 'finished' in fr.other and fr.other['finished']: job.isFinished = lambda *args, **kwargs: True job.isRunning = lambda *args, **kwargs: False self.unknown_status_jobs.remove(job) self.triggers.activate_trigger(JOB_FINISHED, job) return if not self.has_job_running: unstarted_local_jobs = [] for job in self.local_jobs: if not job.isFinished() and not job.killed: unstarted_local_jobs.append(job) if len(unstarted_local_jobs) > 0 and not self.paused: start_job = unstarted_local_jobs.pop(0) self._thread = start_job start_job.finished.connect( lambda data=start_job: self.triggers.activate_trigger( JOB_FINISHED, data)) start_job.started.connect(lambda data=start_job: self.triggers. activate_trigger(JOB_STARTED, data)) start_job.start() else: self._thread = None
class PresetsManager(ProviderManager): """Manager for presets""" def __init__(self, session): self.session = session from . import settings settings.settings = settings._PresetsSettings(session, "presets") settings.settings.triggers.add_handler("setting changed", self._new_custom_folder_cb) self._presets = {} from chimerax.core.triggerset import TriggerSet self.triggers = TriggerSet() self.triggers.add_trigger("presets changed") self._new_custom_folder_cb() if session.ui.is_gui: session.ui.triggers.add_handler('ready', lambda *arg, ses=session: settings. register_settings_options(session)) @property def presets_by_category(self): return { cat: [name for name in info.keys()] for cat, info in self._presets.items() } def preset_function(self, category, preset_name): return self._presets[category][preset_name] def remove_presets(self, category, preset_names): for name in preset_names: del self._presets[category][name] self.triggers.activate_trigger("presets changed", self) def add_presets(self, category, preset_info): """'preset_info' should be a dictionary of preset-name -> callback-function/command-string""" self._add_presets(category, preset_info) self.triggers.activate_trigger("presets changed", self) def add_provider(self, bundle_info, name, order=None, category="General", **kw): from chimerax.core.utils import CustomSortString if order is None: cname = name else: cname = CustomSortString(name, sort_val=int(order)) def cb(name=name, mgr=self, bi=bundle_info): bi.run_provider(self.session, name, self) try: self._presets[category][cname] = cb except KeyError: self._presets[category] = {cname: cb} def end_providers(self): self.triggers.activate_trigger("presets changed", self) def execute(self, preset): if callable(preset): preset() self.session.logger.info( "Preset implemented in Python; no expansion to individual ChimeraX" " commands available.") else: from chimerax.core.commands import run num_lines = 0 with self.session.undo.aggregate("preset"): for line in preset.splitlines(): run(self.session, line, log=False) num_lines += 1 if num_lines == 1: parts = [p.strip() for p in preset.split(';')] display_lines = '\n'.join(parts) else: display_lines = preset self.session.logger.info( 'Preset expands to these ChimeraX commands: ' '<div style="padding-left:4em;padding-top:0px;margin-top:0px">' '<pre style="margin:0;padding:0">%s</pre></div>' % display_lines, is_html=True) def _add_presets(self, category, preset_info): self._presets.setdefault(category, {}).update({ name: lambda p=preset: self.execute(p) for name, preset in preset_info.items() }) def _gather_presets(self, folder): import os, os.path preset_info = {} subfolders = [] for entry in os.listdir(folder): entry_path = os.path.join(folder, entry) if os.path.isdir(entry_path): subfolders.append(entry) continue if entry.endswith(".cxc"): f = open(entry_path, "r") preset_info[entry[:-4].replace('_', ' ')] = f.read() f.close() elif entry.endswith(".py"): from chimerax.core.commands import run, FileNameArg preset_info[entry[:-3].replace('_', ' ')] = lambda p=FileNameArg.unparse(entry_path), \ run=run, ses=self.session: run(ses, "open " + p, log=False) return preset_info, subfolders def _new_custom_folder_cb(self, *args): from .settings import settings if not settings.folder: return import os.path if not os.path.exists(settings.folder): self.session.logger.warning( "Custom presets folder '%s' does not exist" % settings.folder) presets_added = False preset_info, subfolders = self._gather_presets(settings.folder) if preset_info: self._add_presets("Custom", preset_info) presets_added = True for subfolder in subfolders: subpath = os.path.join(settings.folder, subfolder) preset_info, subsubfolders = self._gather_presets(subpath) if preset_info: self._add_presets(subfolder.replace('_', ' '), preset_info) presets_added = True else: self.session.logger.warning( "No presets found in custom preset folder %s" % subpath) if args: # actual trigger callback, rather than startup call if presets_added: self.triggers.activate_trigger("presets changed", self) if not presets_added: self.session.logger.warning( "No presets found in custom preset folder %s" % settings.folder)
class OpenManager(ProviderManager): """Manager for open command""" def __init__(self, session): self.session = session self._openers = {} self._fetchers = {} from chimerax.core.triggerset import TriggerSet self.triggers = TriggerSet() self.triggers.add_trigger("open command changed") def add_provider(self, bundle_info, name, *, type="open", want_path=False, check_path=True, batch=False, format_name=None, is_default=True, synopsis=None, example_ids=None, **kw): logger = self.session.logger bundle_name = _readable_bundle_name(bundle_info) is_default = bool_cvt(is_default, name, bundle_name, "is_default") want_path = bool_cvt(want_path, name, bundle_name, "want_path") check_path = bool_cvt(check_path, name, bundle_name, "check_path") if batch or not check_path: want_path = True type_description = "Open-command" if type == "open" else type.capitalize( ) if kw: logger.warning( "%s provider '%s' supplied unknown keywords in provider" " description: %s" % (type_description, name, repr(kw))) if type == "open": try: data_format = self.session.data_formats[name] except KeyError: logger.warning( "Open-command provider in bundle %s specified unknown" " data format '%s';" " skipping" % (bundle_name, name)) return if data_format in self._openers: logger.warning( "Replacing opener for '%s' from %s bundle with that from" " %s bundle" % (data_format.name, _readable_bundle_name( self._openers[data_format].bundle_info), bundle_name)) self._openers[data_format] = OpenerProviderInfo( bundle_info, name, want_path, check_path, batch) elif type == "fetch": if not name: raise ValueError("Database fetch in bundle %s has empty name" % bundle_name) if len(name) == 1: raise ValueError( "Database fetch '%s' in bundle %s has single-character name which is" " disallowed to avoid confusion with Windows drive letters" % (name, bundle_name)) if format_name is None: raise ValueError( "Database fetch '%s' in bundle %s failed to specify" " file format name" % (name, bundle_name)) try: data_format = self.session.data_formats[format_name] except KeyError: raise ValueError( "Database-fetch provider '%s' in bundle %s specified" " unknown data format '%s'" % (name, bundle_name, format_name)) if name in self._fetchers and format_name in self._fetchers[name]: logger.warning( "Replacing fetcher for '%s' and format %s from %s bundle" " with that from %s bundle" % (name, format_name, _readable_bundle_name( self._fetchers[name][format_name].bundle_info), bundle_name)) if example_ids: example_ids = ",".split(example_ids) else: example_ids = [] if synopsis is None: synopsis = "%s (%s)" % (name.capitalize(), format_name) self._fetchers.setdefault(name, {})[format_name] = FetcherProviderInfo( bundle_info, is_default, example_ids, synopsis) if is_default and len([ fmt for fmt, info in self._fetchers[name].items() if info.is_default ]) > 1: logger.warning( "Multiple default formats declared for database fetch" " '%s'" % name) else: logger.warning( "Unknown provider type '%s' with name '%s' from bundle %s" % (type, name, bundle_name)) def database_info(self, database_name): try: return self._fetchers[database_name] except KeyError: raise NoOpenerError("No such database '%s'" % database_name) @property def database_names(self): return list(self._fetchers.keys()) def end_providers(self): self.triggers.activate_trigger("open command changed", self) def fetch_args(self, database_name, *, format_name=None): try: db_formats = self._fetchers[database_name] except KeyError: raise NoOpenerError("No such database '%s'" % database_name) from chimerax.core.commands import commas if format_name: try: provider_info = db_formats[format_name] except KeyError: # for backwards compatibility, try the nicknames of the format try: df = self.session.data_formats[format_name] except KeyError: nicks = [] else: nicks = df.nicknames + df.name for nick in nicks: try: provider_info = db_formats[nick] format_name = nick except KeyError: continue break else: raise NoOpenerError( "Format '%s' not supported for database '%s'." " Supported formats are: %s" % (format_name, database_name, commas([dbf for dbf in db_formats]))) else: for format_name, provider_info in db_formats.items(): if provider_info.is_default: break else: raise NoOpenerError( "No default format for database '%s'." " Possible formats are: %s" % (database_name, commas([dbf for dbf in db_formats]))) try: args = self.open_args(self.session.data_formats[format_name]) except NoOpenerError: # fetch-only type (e.g. cellPACK) args = {} args.update( provider_info.bundle_info.run_provider(self.session, database_name, self).fetch_args) return args def open_data(self, path, **kw): """ Given a file path and possibly format-specific keywords, return a (models, status message) tuple. The models will not have been opened in the session. The format name can be provided with the 'format' keyword if the filename suffix of the path does not correspond to those for the desired format. """ from .cmd import provider_open return provider_open(self.session, [path], _return_status=True, _add_models=False, **kw) @property def open_data_formats(self): """ The data formats for which an opener function has been registered. """ return list(self._openers.keys()) def open_args(self, data_format): try: provider_info = self._openers[data_format] except KeyError: raise NoOpenerError("No opener registered for format '%s'" % data_format.name) return provider_info.bundle_info.run_provider(self.session, provider_info.name, self).open_args def open_info(self, data_format): try: provider_info = self._openers[data_format] return (provider_info.bundle_info.run_provider( self.session, provider_info.name, self), provider_info) except KeyError: raise NoOpenerError("No opener registered for format '%s'" % data_format.name)
class FormatsManager(ProviderManager): """ Manager for data formats. Manager can also be used as if it were a { format-name -> data format } dictionary. """ CAT_SCRIPT = "Command script" CAT_SESSION = "Session" CAT_GENERAL = "General" def __init__(self, session): self.session = session self._formats = {} self._suffix_to_formats = {} from chimerax.core.triggerset import TriggerSet self.triggers = TriggerSet() self.triggers.add_trigger("data formats changed") def add_format(self, name, category, *, suffixes=None, nicknames=None, bundle_info=None, mime_types=None, reference_url=None, insecure=None, encoding=None, synopsis=None, allow_directory=False, raise_trigger=True): def convert_arg(arg, default=None): if arg and isinstance(arg, str): return arg.split(',') return [] if default is None else default suffixes = convert_arg(suffixes) nicknames = convert_arg(nicknames, [name.lower()]) mime_types = convert_arg(mime_types) insecure = category == self.CAT_SCRIPT if insecure is None else insecure logger = self.session.logger if name in self._formats: registrant = lambda bi: "unknown registrant" \ if bi is None else "%s bundle" % bi.name logger.info( "Replacing data format '%s' as defined by %s with definition" " from %s" % (name, registrant( self._formats[name][0]), registrant(bundle_info))) from .format import DataFormat data_format = DataFormat(name, category, suffixes, nicknames, mime_types, reference_url, insecure, encoding, synopsis, allow_directory) for suffix in suffixes: self._suffix_to_formats.setdefault(suffix, []).append(data_format) self._formats[name] = (bundle_info, data_format) if raise_trigger: self.triggers.activate_trigger("data formats changed", self) def add_provider(self, bundle_info, name, *, category=None, suffixes=None, nicknames=None, mime_types=None, reference_url=None, insecure=None, encoding=None, synopsis=None, allow_directory=False, **kw): logger = self.session.logger if kw: logger.warning( "Data format provider '%s' supplied unknown keywords with format" " description: %s" % (name, repr(kw))) if category is None: logger.warning( "Data format provider '%s' didn't specify a category." " Using catch-all category '%s'" % (name, self.CAT_GENERAL)) category = self.CAT_GENERAL self.add_format(name, category, suffixes=suffixes, nicknames=nicknames, bundle_info=bundle_info, mime_types=mime_types, reference_url=reference_url, insecure=insecure, encoding=encoding, synopsis=synopsis, allow_directory=allow_directory, raise_trigger=False) def open_format_from_suffix(self, suffix): """ Given a file suffix (starting with a '.'), return the corresponding openable data format. Returns None if there is no such format. """ from chimerax.open_command import NoOpenerError return self._format_from_suffix(self.session.open_command.open_info, NoOpenerError, suffix) def open_format_from_file_name(self, file_name): """ Given a file name, return the corresponding openable data format. Raises NoFormatError if there is no such format. """ "Return data format based on file_name's suffix, ignoring compression suffixes" return self._format_from_filename(self.open_format_from_suffix, file_name) def qt_file_filter(self, fmt): """ Given a data format 'fmt', return a string usable as a member of the list argument used with the setNameFilters() method of a Qt file dialog. """ return "%s (%s)" % (fmt.synopsis, "*" + " *".join(fmt.suffixes)) def save_format_from_suffix(self, suffix): """ Given a file suffix (starting with a '.'), return the corresponding savable data format. Returns None if there is no such format. """ from chimerax.save_command import NoSaverError return self._format_from_suffix(self.session.save_command.save_info, NoSaverError, suffix) def save_format_from_file_name(self, file_name): """ Given a file name, return the corresponding saveable data format. Raises NoFormatError if there is no such format. """ "Return data format based on file_name's suffix, ignoring compression suffixes" return self._format_from_filename(self.save_format_from_suffix, file_name) @property def formats(self): """ Returns a list of all known data formats """ return [info[1] for info in self._formats.values()] def end_providers(self): self.triggers.activate_trigger("data formats changed", self) def __getitem__(self, key): if not isinstance(key, str): raise TypeError("Data format key is not a string") if key in self._formats: return self._formats[key][1] for bi, format_data in self._formats.values(): if key in format_data.nicknames: return format_data raise KeyError("No known data format '%s'" % key) def __len__(self): return len(self._formats) def __iter__(self): '''iterator over models''' return iter(self.formats) def _format_from_filename(self, suffix_func, file_name): if '.' in file_name: from chimerax import io base_name = io.remove_compression_suffix(file_name) from os.path import splitext root, ext = splitext(base_name) if not ext: raise NoFormatError( "'%s' has only compression suffix; cannot determine" " format from suffix" % file_name) data_format = suffix_func(ext) if not data_format: raise NoFormatError( "No known data format for file suffix '%s'" % ext) else: raise NoFormatError("Cannot determine format for '%s'" % file_name) return data_format def _format_from_suffix(self, info_func, error_type, suffix): if '#' in suffix: suffix = suffix[:suffix.index('#')] try: formats = self._suffix_to_formats[suffix] except KeyError: return None for fmt in formats: try: info_func(fmt) except error_type: pass else: return fmt return None
class MapSetBase(Model): ''' Base class for XmapSet_Live, XmapSet_Static and NXmapSet. Provides basic methods for visualisation, masking etc. ''' # Default contour levels and colours for generic maps. Override in the # derived class if you wish STANDARD_LOW_CONTOUR = numpy.array([1.5]) STANDARD_HIGH_CONTOUR = numpy.array([2.5]) STANDARD_DIFFERENCE_MAP_CONTOURS = numpy.array([-3.0, 3.0]) DEFAULT_MESH_MAP_COLOR = [0,1.0,1.0,1.0] # Solid cyan DEFAULT_SOLID_MAP_COLOR = [0,1.0,1.0,0.4] # Transparent cyan DEFAULT_DIFF_MAP_COLORS = [[1.0,0,0,1.0],[0,1.0,0,1.0]] #Solid red and green def __init__(self, manager, name): super().__init__(name, manager.session) self._mgr = manager if not hasattr(self, 'triggers'): from chimerax.core.triggerset import TriggerSet self.triggers = TriggerSet() trigger_names = ( 'map box changed', 'map box moved', ) for t in trigger_names: self.triggers.add_trigger(t) mh = self._mgr_handlers = [] mh.append((manager, manager.triggers.add_handler('spotlight moved', self._box_moved_cb))) mh.append((manager, manager.triggers.add_handler('spotlight changed', self._box_changed_cb))) mh.append((manager, manager.triggers.add_handler('cover coords', self._cover_coords_cb))) # @property # def triggers(self): # return self._triggers @property def master_map_mgr(self): return self._mgr @property def box_center(self): return self.master_map_mgr.box_center @property def crystal_mgr(self): return self.master_map_mgr.crystal_mgr @property def structure(self): return self.crystal_mgr.structure @property def hklinfo(self): return self.crystal_mgr.hklinfo @property def spacegroup(self): return self.crystal_mgr.spacegroup @property def cell(self): return self.crystal_mgr.cell @property def grid(self): return self.crystal_mgr.grid @property def display_radius(self): '''Get/set the radius (in Angstroms) of the live map display sphere.''' return self.master_map_mgr.spotlight_radius def __getitem__(self, name_or_index): '''Get one of the child maps by name or index.''' if type(name_or_index) == str: for m in self.child_models(): if m.name == name_or_index: return m raise KeyError('No map with the name "{}"!'.format(name_or_index)) else: return self.child_models()[name_or_index] @property def all_maps(self): from chimerax.map import Volume return [v for v in self.child_models() if isinstance(v, Volume)] @property def spotlight_mode(self): return self.master_map_mgr.spotlight_mode @spotlight_mode.setter def spotlight_mode(self, switch): raise NotImplementedError('Spotlight mode can only be enabled/disabled ' 'via the master symmetry manager!') @property def spotlight_center(self): return self.master_map_mgr.spotlight_center @spotlight_center.setter def spotlight_center(self, *_): raise NotImplementedError('Spotlight centre can only be changed ' 'via the master symmetry manager!') def expand_to_cover_coords(self, coords, padding): raise NotImplementedError('Function not defined in the base class!') # Callbacks def _cover_coords_cb(self, trigger_name, data): coords, padding = data self.expand_to_cover_coords(coords, padding) def _box_changed_cb(self, trigger_name, data): ''' By default, just re-fires with the same data. Override in derived class if more complex handling is needed ''' self.triggers.activate_trigger('map box changed', data) def _box_moved_cb(self, trigger_name, data): ''' By default, just re-fires with the same data. Override in derived class if more complex handling is needed ''' self.triggers.activate_trigger('map box moved', data) def delete(self): for (mgr, h) in self._mgr_handlers: try: mgr.triggers.remove_handler(h) except: continue super().delete()
class BondRotationManager(StateManager): """Manager for bond rotations""" CREATED, MODIFIED, REVERSED, DELETED = trigger_names = ("created", "modified", "reversed", "deleted") def __init__(self, session, bundle_info): self.bond_rotations = {} # bond -> BondRotation self.bond_rotaters = {} # ident -> BondRotater self.session = session from chimerax.core.triggerset import TriggerSet self.triggers = TriggerSet() for trig_name in self.trigger_names: self.triggers.add_trigger(trig_name) def delete_rotation(self, rotater): del self.bond_rotaters[rotater.ident] rotation = rotater.rotation rotation.rotaters.remove(rotater) if not rotation.rotaters: del self.bond_rotations[rotation.bond] if not self.bond_rotations: from chimerax.atomic import get_triggers get_triggers().remove_handler(self._handler_ID) if not rotater.one_shot: self.triggers.activate_trigger(self.DELETED, rotater) def delete_all_rotations(self): for rotater in self.bond_rotaters.values(): self.triggers.activate_trigger(self.DELETED, rotater) if self.bond_rotations: from chimerax.atomic import get_triggers get_triggers().remove_handler(self._handler_ID) for rotation in self.bond_rotations.values(): # break reference loops rotation.rotaters = [] self.bond_rotations.clear() self.bond_rotaters.clear() clear = delete_all_rotations def new_rotation(self, bond, ident=None, move_smaller_side=True, one_shot=True): """Create bond rotation for 'bond' Parameters ---------- bond : Bond Bond to rotate ident: an integer or None Number used to refer to bond rotation in commands. If None, automatically assign one. move_smaller_side: bool If True, move the "smaller" side (side with fewer atoms attached) when the bond rotation moves. Otherwise move the bigger side. one_shot: bool True if the rotation is going to be used to change the torsion and then immediately deleted -- so don't bother firing triggers. Returns the created BondRotater """ if ident is None: ident = 1 while ident in self.bond_rotaters: ident += 1 elif ident in self.bond_rotaters: raise BondRotationError("Bond rotation identifier %s already in use" % ident) try: moving_side = bond.smaller_side except ValueError: raise BondRotationError("Bond %s is part of a ring/cycle and cannot be rotated" % bond) if not move_smaller_side: moving_side = bond.other_atom(moving_side) if not self.bond_rotations: from chimerax.atomic import get_triggers self._handler_ID = get_triggers().add_handler('changes', self._changes_cb) if bond in self.bond_rotations: rotation = self.bond_rotations[bond] else: from .bond_rot import BondRotation rotation = BondRotation(self.session, bond) self.bond_rotations[bond] = rotation rotater = rotation.new_rotater(ident, moving_side, one_shot) self.bond_rotaters[ident] = rotater if not one_shot: self.triggers.activate_trigger(self.CREATED, rotater) self.session.logger.status("Bond rotation identifier is %s" % ident, log=True) return rotater def rotation_for_ident(self, ident): try: return self.bond_rotaters[ident] except KeyError: raise BondRotationError("No such bond rotation ident: %s" % ident) def _changes_cb(self, trig_name, changes): if changes.num_deleted_bonds() > 0: for br in list(self.bond_rotations.values()): if br.bond.deleted: self._delete_rotation(br) new_bonds = changes.created_bonds(include_new_structures=False) if new_bonds: nb_structures = new_bonds.unique_structures for br in list(self.bond_rotations.values()): if br.bond.structure in nb_structures: if br.bond.rings(cross_residue=True): # new bond closed a cycle involving bond-rotation bond self._delete_rotation(br) changed_structures = set() if 'active_coordset changed' in changes.structure_reasons(): changed_structures.update(changes.modified_structures()) if 'coordset changed' in changes.coordset_reasons(): changed_structures.update(changes.modified_coordsets().unique_structures) if 'coord changed' in changes.atom_reasons(): changed_structures.update(changes.modified_atoms().unique_structures) if changed_structures: for br in self.bond_rotaters.values(): if not br.one_shot and br.rotation.bond.structure in changed_structures: self.triggers.activate_trigger(self.MODIFIED, br) def _delete_rotation(self, br): # delete a BondRotation rather than a BondRotater for rotater in br.rotaters[:]: self.delete_rotation(rotater) # session methods def reset_state(self, session): self.clear() @staticmethod def restore_snapshot(session, data): mgr = session.bond_rotations mgr._ses_restore(data) return mgr def take_snapshot(self, session, flags): # viewer_info is "session independent" return { 'version': 1, 'rotations': self.bond_rotations, 'rotaters': self.bond_rotaters, } def _ses_restore(self, data): self.clear() if 'bond_rots' in data: # old, non-backwards compatible, session data if data['bond_rots']: self.session.logger.warning('Bond-rotation data in session is obsolete and not restorable;' " skipping") return self.bond_rotations = data['rotations'] self.bond_rotaters = data['rotaters'] if self.bond_rotations: from chimerax.atomic import get_triggers self._handler_ID = get_triggers().add_handler('changes', self._changes_cb)
class MapMgr(Model): ''' Top-level manager for all maps associated with a model. ''' DEFAULT_MAX_VOXELS = 750000 # SESSION_SAVE=False def __init__(self, crystal_manager, spotlight_radius=12, default_oversampling_rate=2.0, auto_add = True): cm = self._mgr = crystal_manager super().__init__('Map Manager', cm.session) self._default_oversampling_rate=default_oversampling_rate self._zone_mgr = None if not hasattr(self, 'triggers'): from chimerax.core.triggerset import TriggerSet self.triggers = TriggerSet() trigger_names = ( # Deprecated 'map box changed', # Ask each MapSet to expand its volumes to cover an arbitrary set # of coordinates as efficiently as possible. 'cover coords', # Change the radius of the "spotlight" sphere. It is up to each # MapSet to work out how to accommodate it 'spotlight changed', 'spotlight moved', # Just changed the centre of the box ) for t in trigger_names: self.triggers.add_trigger(t) self._max_voxels_for_live_remask = self.DEFAULT_MAX_VOXELS # Handler for live box update self._box_update_handler = None # Is the map box moving with the centre of rotation? self._spotlight_center = None self._initialize_zone_mgr() # Radius of the sphere in which the map will be displayed when # in live-scrolling mode self.spotlight_radius = spotlight_radius if self.spotlight_mode: self._start_spotlight_mode() # self.display=False self._rezone_pending = False # Apply the surface mask mh = self._mgr_handlers = [] mh.append((cm, cm.triggers.add_handler('mode changed', self._spotlight_mode_changed_cb))) # self.session.triggers.add_handler('frame drawn', self._first_init_cb) if auto_add: cm.add([self]) # def added_to_session(self, session): # super().added_to_session(session) # session.triggers.add_handler('frame drawn', self._first_init_cb) def _initialize_zone_mgr(self): if self._zone_mgr is None: from .mask_handler import ZoneMgr coords = [self.spotlight_center] self._zone_mgr = ZoneMgr(self.session, 1.5, 5) @property def zone_mgr(self): return self._zone_mgr @property def xmapsets(self): ''' Sets of crystallographic maps associated with this model. Each XmapSet handles the set of maps derived from a single crystallographic dataset. ''' from .xmapset import XmapSet return [m for m in self.child_models() if isinstance(m, XmapSet)] @property def nxmapset(self): ''' Handler for all real-space (non-crystallographic) maps associated with this model. ''' from .nxmapset import NXmapSet for m in self.child_models(): if isinstance(m, NXmapSet): return m return NXmapSet(self, 'Non-crystallographic maps') @property def all_xtal_maps(self): from .map_handler_base import XmapHandlerBase return [m for m in self.all_models() if isinstance(m, XmapHandlerBase)] @property def all_non_xtal_maps(self): from .nxmapset import NXmapHandler return [m for m in self.all_models() if isinstance(m, NXmapHandler)] @property def all_maps(self): from chimerax.map import Volume return [m for m in self.all_models() if isinstance(m, Volume)] @property def all_surfaces(self): return [s for m in self.all_maps for s in m.surfaces] @property def crystal_mgr(self): return self._mgr @property def box_center(self): return self.crystal_mgr.spotlight_center @property def structure(self): return self.crystal_mgr.structure # @property # def triggers(self): # return self._triggers @property def spacegroup(self): return self.crystal_mgr.spacegroup @property def cell(self): return self.crystal_mgr.cell @property def spotlight_radius(self): '''Get/set the radius (in Angstroms) of the live map display sphere.''' return self._spotlight_radius @spotlight_radius.setter def spotlight_radius(self, radius): import numpy self._spotlight_radius = radius center = self.crystal_mgr.spotlight_center self.triggers.activate_trigger('spotlight changed', (center, radius) ) # self._surface_zone.update(radius, coords = numpy.array([center])) self._zone_mgr.radius = radius self._zone_mgr.pad = radius # self._reapply_zone() @property def box_params(self): return (self._box_corner_xyz, self._box_corner_grid, self._box_dimensions) @property def spotlight_mode(self): ''' Is live map scrolling turned on? Can only be changed via the master symmetry manager. ''' return self.crystal_mgr.spotlight_mode @spotlight_mode.setter def spotlight_mode(self, switch): raise NotImplementedError( 'Mode can only be changed via the master symmetry manager!') @property def spotlight_center(self): ''' Current (x,y,z) position of the centre of the "spotlight". Read-only. ''' return self.crystal_mgr.spotlight_center @property def last_covered_selection(self): ''' Last set of coordinates covered by `self.crystal_mgr.isolate_and_cover_selection()`. Read-only. ''' return self.crystal_mgr.last_covered_selection @property def display(self): return super().display @display.setter def display(self, switch): Model.display.fset(self, switch) if switch: if self.spotlight_mode: self._start_spotlight_mode() self._reapply_zone() def add_xmapset_from_mtz(self, mtzfile, oversampling_rate=None, auto_choose_reflection_data=True): if oversampling_rate is None: oversampling_rate = self._default_oversampling_rate return self.add_xmapset_from_file(mtzfile, oversampling_rate, auto_choose_reflection_data) def add_xmapset_from_file(self, sffile, oversampling_rate=None, auto_choose_reflection_data=True): if oversampling_rate is None: oversampling_rate = self._default_oversampling_rate from ..clipper_mtz import ReflectionDataContainer mtzdata = ReflectionDataContainer(self.session, sffile, shannon_rate = oversampling_rate, auto_choose_reflection_data=auto_choose_reflection_data) cm = self.crystal_mgr if not cm.has_symmetry: self.session.logger.info('(CLIPPER) NOTE: No symmetry information found ' 'in model. Using symmetry from MTZ file.') cm = self.crystal_mgr cm.add_symmetry_info(mtzdata.cell, mtzdata.spacegroup, mtzdata.grid_sampling, mtzdata.resolution) elif not self.symmetry_matches(mtzdata): raise RuntimeError('Symmetry info from MTZ file does not match ' 'symmetry info from model!') from .xmapset import XmapSet return XmapSet(self, mtzdata) def symmetry_matches(self, xtal_data): return ( xtal_data.cell.equals(self.cell, 1.0) and xtal_data.spacegroup.spacegroup_number == self.spacegroup.spacegroup_number ) def _spotlight_mode_changed_cb(self, *_): if self.spotlight_mode: self._start_spotlight_mode() else: self._stop_spotlight_mode() def _start_spotlight_mode(self): zm = self._zone_mgr zm.radius = zm.pad = self.spotlight_radius zm.allow_remask_on_coord_updates() # zm.coords = [self.spotlight_center] self.triggers.activate_trigger('spotlight changed', (self.spotlight_center, self.spotlight_radius) ) if self._box_update_handler is None: self._box_update_handler = self.crystal_mgr.triggers.add_handler( 'spotlight moved', self.update_spotlight) self.update_spotlight(None, self.spotlight_center) from chimerax.geometry import Places self.positions = Places() # self._reapply_zone() def _stop_spotlight_mode(self): if self._box_update_handler is not None: self.crystal_mgr.triggers.remove_handler(self._box_update_handler) self._box_update_handler = None def cover_box(self, minmax): ''' Set the map box to fill a volume encompassed by the provided minimum and maximum xyz coordinates. Automatically turns off live scrolling. ''' self.spotlight_mode = False xyz_min, xyz_max = minmax center = (xyz_min + xyz_max)/2 self.triggers.activate_trigger('map box changed', (center, xyz_min, xyz_max)) def cover_atoms(self, atoms, transforms=None, transform_indices=None, mask_radius=3, extra_padding=12): ''' Expand all maps to a region large enough to cover the atoms, plus mask_radius+extra_padding in every direction. If provided, transforms should be a Places object, and transform_indices a Numpy array giving the index of the Place to be used to transform the coordinates of each atom. Unlike cover_coords(), the mask will be periodically updated in response to atom movements. ''' zm = self._zone_mgr zm.set_symmetry_map(atoms, transforms, transform_indices) zm.radius = mask_radius zm.pad = extra_padding self.triggers.activate_trigger('cover coords', (zm.coords, mask_radius+extra_padding)) displayed_volumes = [v for v in self.all_maps if v.display] num_displayed_voxels = sum(v.region_matrix(v.region).size for v in displayed_volumes) if num_displayed_voxels > self._max_voxels_for_live_remask: zm.block_remask_on_coord_updates() else: zm.allow_remask_on_coord_updates() # self._reapply_zone() def cover_coords(self, coords, mask_radius=3, extra_padding=3): ''' Expand all maps to a region large enough to cover the coords, plus mask_radius+extra_padding in every direction. ''' self.triggers.activate_trigger('cover coords', (coords, mask_radius+extra_padding)) zm = self._zone_mgr zm.coords = coords zm.radius = mask_radius zm.pad = extra_padding # self._surface_zone.update(mask_radius, coords=coords) # self._reapply_zone() def update_zone_mask(self, coords): self._zone_mgr.coords = coords def update_spotlight(self, trigger_name, new_center): ''' Update the position of the "spotlight" to surround the current centre of rotation. If this manager is not currently displayed, then filling the volumes with data around the new position will be deferred unless force is set to True. ''' if self.spotlight_mode: import numpy self.triggers.activate_trigger('spotlight moved', new_center) zm = self._zone_mgr zm.coords = numpy.array([new_center]) # self._surface_zone.update(self.spotlight_radius, coords = numpy.array([new_center])) # self._reapply_zone() def rezone_needed(self): if not self._rezone_pending: self._rezone_pending=True self.session.triggers.add_handler('new frame', self._rezone_once_cb) # Callbacks def _first_init_cb(self, *_): self.display = True self.update_spotlight(_, self.spotlight_center) from chimerax.core.triggerset import DEREGISTER return DEREGISTER def _rezone_once_cb(self, *_): self._reapply_zone() self._rezone_pending=False from chimerax.core.triggerset import DEREGISTER return DEREGISTER def _reapply_zone(self): ''' Reapply any surface zone applied to the volume after changing box position. ''' from .mask_handler import ZoneMask for s in self.all_surfaces: asm = s.auto_remask_triangles if asm is None: ZoneMask(s, self._zone_mgr, None) else: asm() def delete(self): self._stop_spotlight_mode() for (mgr, h) in self._mgr_handlers: try: mgr.triggers.remove_handler(h) except: continue super().delete() def take_snapshot(self, session, flags): from chimerax.core.models import Model data = { 'symmetry manager': self._mgr, 'model state': Model.take_snapshot(self, session, flags) } from .. import CLIPPER_STATE_VERSION data['version']=CLIPPER_STATE_VERSION return data @staticmethod def restore_snapshot(session, data): from chimerax.core.models import Model sh = data['symmetry manager'] if sh is None: return None mmgr = MapMgr(sh, auto_add=False) Model.set_state_from_snapshot(mmgr, session, data['model state']) session.triggers.add_handler('end restore session', mmgr._end_restore_session_cb) return mmgr def _end_restore_session_cb(self, *_): self._reapply_zone() from chimerax.core.triggerset import DEREGISTER return DEREGISTER
class SaveManager(ProviderManager): """Manager for save command""" def __init__(self, session): self.session = session self._savers = {} from chimerax.core.triggerset import TriggerSet self.triggers = TriggerSet() self.triggers.add_trigger("save command changed") def add_provider(self, bundle_info, format_name, compression_okay=True, **kw): logger = self.session.logger bundle_name = _readable_bundle_name(bundle_info) if kw: logger.warning( "Save-command provider '%s' supplied unknown keywords in" " provider description: %s" % (name, repr(kw))) try: data_format = self.session.data_formats[format_name] except KeyError: logger.warning( "Save-command provider in bundle %s specified unknown data" " format '%s'; skipping" % (bundle_name, format_name)) return if data_format in self._savers: logger.warning( "Replacing file-saver for '%s' from %s bundle with that from" " %s bundle" % (data_format.name, _readable_bundle_name( self._savers[data_format].bundle_info), bundle_name)) self._savers[data_format] = ProviderInfo( bundle_info, format_name, bool_cvt(compression_okay, format_name, bundle_name, "compression_okay")) def end_providers(self): self.triggers.activate_trigger("save command changed", self) def save_args(self, data_format): try: provider_info = self._savers[data_format] except KeyError: raise NoSaverError("No file-saver registered for format '%s'" % data_format.name) return provider_info.bundle_info.run_provider( self.session, provider_info.format_name, self).save_args def hidden_args(self, data_format): try: provider_info = self._savers[data_format] except KeyError: raise NoSaverError("No file-saver registered for format '%s'" % data_format.name) return provider_info.bundle_info.run_provider( self.session, provider_info.format_name, self).hidden_args def save_args_widget(self, data_format): try: provider_info = self._savers[data_format] except KeyError: raise NoSaverError("No file-saver registered for format '%s'" % data_format.name) return provider_info.bundle_info.run_provider( self.session, provider_info.format_name, self).save_args_widget(self.session) def save_args_string_from_widget(self, data_format, widget): try: provider_info = self._savers[data_format] except KeyError: raise NoSaverError("No file-saver registered for format '%s'" % data_format.name) return provider_info.bundle_info.run_provider( self.session, provider_info.format_name, self).save_args_string_from_widget(widget) def save_data(self, path, **kw): """ Given a file path and possibly format-specific keywords, save a data file based on the current session. The format name can be provided with the 'format' keyword if the filename suffix of the path does not correspond to those for the desired format. """ from .cmd import provider_save provider_save(self.session, path, **kw) @property def save_data_formats(self): """ The names of data formats for which an saver function has been registered. """ return list(self._savers.keys()) def save_info(self, data_format): try: return self._savers[data_format] except KeyError: raise NoSaverError("No file-saver registered for format '%s'" % data_format.name)