Example #1
0
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)
Example #2
0
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()
Example #3
0
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)
Example #4
0
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)
Example #6
0
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
Example #7
0
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)
Example #8
0
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
Example #9
0
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)
Example #10
0
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)
Example #11
0
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
Example #12
0
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()
Example #13
0
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)
Example #14
0
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
Example #15
0
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)