Source code for schrodinger.trajectory.trajectory_gui_dir.interaction_plots

"""
Plot Manager for interaction plots (e.g. H-Bonds, Pi-Pi Stacking)
"""
from collections import defaultdict
from schrodinger.structutils import analyze
from schrodinger.ui import maestro_ui
from schrodinger.tasks import queue
from . import traj_plot_models
from . import plots
from .traj_plot_models import AnalysisMode
from schrodinger.Qt import QtCore
from schrodinger.Qt.QtCore import Qt
from schrodinger import get_maestro

maestro = get_maestro()

# This message is shown below every interactions error dialog text:
ATOM_DEFINITION_MESSAGE = ("\nCheck the Interactions pane in the\n"
                           "Workspace Configuration toolbar for\n"
                           "the current atom set definitions.")

INTERACTIONS_DEF_EMPTY_ALIST_ERROR = (
    "The atoms specified for this interaction\n"
    "type do not appear in the trajectory.\n") + ATOM_DEFINITION_MESSAGE

MISSING_INTERACTION_TYPES_ERROR = (
    "No instances of these interaction types were found in any frame."
) + ATOM_DEFINITION_MESSAGE

NO_CUSTOM_ASLS_DEFINED_ERR = (
    "The 'Other' type interaction definition is selected "
    "but ASLs are not defined. Please define them or select 'Ligand-Complex' "
    "or 'Intra-Ligand' and try again.")

# Interaction Mode constants
LIGAND_RECEPTOR_ASL = 'ligand OR protein'
INTRA_LIGAND_ASL = 'ligand'
ALL_ASL = 'all'

INTERACTION_ASL_TO_TITLE = {
    LIGAND_RECEPTOR_ASL: 'Ligand-Receptor',
    INTRA_LIGAND_ASL: 'Intra-Ligand',
    ALL_ASL: 'All'
}
LIGAND_RECEPTOR_DISPLAY_TO_TITLE = {
    maestro_ui.LigandReceptorInteractions.ALL_AVAILABLE: 'All Available',
    maestro_ui.LigandReceptorInteractions.SAME_ENTRY_ONLY: 'Same Entry Only',
    maestro_ui.LigandReceptorInteractions.SAME_GROUP_ONLY: 'Same Group Only',
    maestro_ui.LigandReceptorInteractions.NONE: 'None'
}


[docs]class InteractionsPlotManager(plots.TrajectoryAnalysisPlotManager): """ Class responsible for setting up and running an interactions plot task. """
[docs] def __init__(self, panel, mode, use_visible_atoms): """ :param panel: Parent panel :type panel: QtWidget.QWidget :param mode: Analysis mode :type mode: AnalysisMode :param use_visible_atoms: Whether to only show interactions between visible atoms. :type use_visible_atoms: bool """ super().__init__(panel) self.use_visible_atoms = use_visible_atoms self.task = traj_plot_models.TrajectoryAnalysisSubprocTask() ws_st = maestro.workspace_get() self.configureTask(self.task, mode, ws_st) self.setupView()
[docs] def configureTask(self, task, analysis_mode, ws_st): """ Configure the analysis task analyze Workspace interactions of given type. :param task: Task to configure :type task: tasks.AbstractTask :param analysis_mode: Interactions analysis mode for this task :type analysis_mode: traj_plot_models.AnalysisMode :param ws_st: Workspace structure containing the system. :type ws_st: structure.Structure :return: Configured analysis task :rtype: traj_plot_model.TrajectoryAnalysisSubprocTask """ super().configureTask(task) task.input.analysis_mode = analysis_mode maestro_hub = maestro_ui.MaestroHub.instance() interactions_state = maestro_hub.getInteractionViewState() if analysis_mode in [ AnalysisMode.HydrogenBondFinder, AnalysisMode.HalogenBondFinder, AnalysisMode.SaltBridgeFinder ]: anums_as_kwargs = False interactions_mode = interactions_state.non_covalent_mode elif analysis_mode in [ AnalysisMode.PiPiFinder, AnalysisMode.CatPiFinder ]: anums_as_kwargs = True interactions_mode = interactions_state.pi_mode else: raise ValueError( f"{analysis_mode} is not a valid interactions analyzer.") if interactions_mode == maestro_ui.InteractionMode.LIGAND_RECEPTOR: fit_asl = LIGAND_RECEPTOR_ASL anums1 = analyze.evaluate_asl(ws_st, 'ligand') anums2 = analyze.evaluate_asl(ws_st, 'NOT ligand') elif interactions_mode == maestro_ui.InteractionMode.INTRA_LIGAND: fit_asl = INTRA_LIGAND_ASL anums1 = analyze.evaluate_asl(ws_st, INTRA_LIGAND_ASL) anums2 = anums1[:] elif interactions_mode == maestro_ui.InteractionMode.ALL: fit_asl = ALL_ASL anums1 = analyze.evaluate_asl(ws_st, ALL_ASL) anums2 = anums1[:] elif interactions_mode == maestro_ui.InteractionMode.OTHER: asl1 = interactions_state.non_covalent_bonds_other_asl_set1 asl2 = interactions_state.non_covalent_bonds_other_asl_set2 if not asl1 or not asl2: # NOTE: Typically both ASLs will be defined or neither defined. raise RuntimeError(NO_CUSTOM_ASLS_DEFINED_ERR) fit_asl = f'({asl1}) OR ({asl2})' anums1 = analyze.evaluate_asl(ws_st, asl1) anums2 = analyze.evaluate_asl(ws_st, asl2) else: raise AssertionError("Unexpected interaction mode: %s" % interactions_mode) if self.use_visible_atoms: # Only consider visible atoms in the Workspace for interactions anums1 = [a for a in anums1 if ws_st.atom[a].visible] anums2 = [a for a in anums2 if ws_st.atom[a].visible] if len(anums1) == 0 or len(anums2) == 0: mode_name = traj_plot_models.ANALYSIS_MODE_MAP[analysis_mode].name msg = f"Cannot display {mode_name} plot.\n{INTERACTIONS_DEF_EMPTY_ALIST_ERROR}" raise RuntimeError(msg) anums1.sort() anums2.sort() # Used for settings hash: self.anums1 = anums1 self.anums2 = anums2 if anums_as_kwargs: task.input.additional_kwargs = {"aids1": anums1, "aids2": anums2} else: task.input.additional_args = [anums1, anums2] task.input.fit_asl = fit_asl
[docs] def getInitialPlotTitleAndTooltip(self): """ Derive the interaction plot title based on the settings stored in task. See base method for documentation of return value. """ task = self.task interactions_mode = INTERACTION_ASL_TO_TITLE[task.input.fit_asl] mode = task.input.analysis_mode if mode == AnalysisMode.HydrogenBondFinder: prefix = 'Hydrogen Bonds' elif mode == AnalysisMode.HalogenBondFinder: prefix = 'Halogen Bonds' elif mode == AnalysisMode.SaltBridgeFinder: prefix = 'Salt Bridges' elif mode == AnalysisMode.PiPiFinder: prefix = 'Pi-Pi Stacking' elif mode == AnalysisMode.CatPiFinder: prefix = 'Pi-Cation' title = f'{prefix} ({interactions_mode})' # TODO: consider showing more information in a tooltip instead of # returning None return title, None
[docs] def formatPlotAxes(self): """ Formats axes tick numbers and spacing depending on result data. """ results = self.task.output.result self._formatPlotAxes(results)
def _formatPlotAxes(self, result_data): """ Shared between both classes. """ chart = self.chart() chart.createDefaultAxes() axes = chart.axes() for axis in axes: if axis.orientation() == Qt.Orientation.Horizontal: axis.setTitleText('Time (ns)') axis.applyNiceNumbers() axis.setLabelFormat('%i') interstitial_values = max(result_data) - min(result_data) + 1 num_ticks = min(plots.MAX_AXIS_TICKS, interstitial_values) axis.setTickCount(num_ticks) else: axis.setTitleText('Number of Interactions') axis.setLabelFormat('%.1f') plots._generateAxisSpecifications(result_data, axis) def _validateTask(self, task): if not super()._validateTask(task): return False if all(v == 0 for v in task.output.result): return False return True
[docs]class MultiSeriesPlotManager(InteractionsPlotManager): """ Plot manager that contains multiple interaction series, on a single plot. Raises RuntimeError if plot could not be created. """ newDataAvailable = QtCore.pyqtSignal() showWarning = QtCore.pyqtSignal(str)
[docs] def __init__(self, panel, use_visible_atoms): plots.TrajectoryAnalysisPlotManager.__init__(self, panel) self.use_visible_atoms = use_visible_atoms self.task_queue = self.createBatchQueue() self.task_queue.mainDone.connect(self._onTaskQueueFinished) # Make the view a little taller, to allow room for the legend: self.setupView(fixed_height=350, multi_series=True)
[docs] def createBatchQueue(self): """ Get a configured task queue for running a batch of tasks. :return: Configured task queue :rtype: queue.TaskQueue """ task_queue = queue.TaskQueue() task_queue.max_running_tasks = 1 hub = maestro_ui.MaestroHub.instance() ws_st = maestro.workspace_get() intview = hub.getInteractionViewState() for attr, mode in [('halogen_bonds', AnalysisMode.HalogenBondFinder), ('hydrogen_bonds', AnalysisMode.HydrogenBondFinder), ('salt_bridges', AnalysisMode.SaltBridgeFinder), ('pi_stacking', AnalysisMode.PiPiFinder), ('pi_cation', AnalysisMode.CatPiFinder)]: if getattr(intview, attr) is True: task = traj_plot_models.TrajectoryAnalysisSubprocTask() try: self.configureTask(task, mode, ws_st) except RuntimeError: continue task.input.for_multiseries_plot = True task_queue.addTask(task) if len(task_queue.getTasks()) == 0: msg = f"Cannot display interaction plots.\n{MISSING_INTERACTION_TYPES_ERROR}" raise RuntimeError(msg) return task_queue
[docs] def start(self): self.task_queue.start()
[docs] def getSettingsHash(self): """ Generates a key for use in the interactions map :return: key for interaction map :rtype: str, tuple(AnalysisMode), tuple(str) """ # Tasks in a TaskQueue should be exposed (PANEL-18939) tasks = self.task_queue.getTasks() all_tasks_settings = [] for task in tasks: all_tasks_settings.append([ task.input.analysis_mode.name, task.input.additional_args, task.input.additional_kwargs ]) return self.generateSettingsHash(all_tasks_settings)
[docs] def createCollapsiblePlotWidget(self): plot_widget = super().createCollapsiblePlotWidget() self.newDataAvailable.connect(plot_widget.onPlotTitleChanged) return plot_widget
def _onTaskQueueFinished(self): """ Warns the user that some interactions in a multiseries analysis did not generate plot data. Updates Interaction Plot cache with plot """ msg = self.validateTaskQueue(self.task_queue) if msg: self.showWarning.emit(msg)
[docs] def validateTaskQueue(self, task_queue): """ Validate task queue for multi-interaction plot. On error, return error message text, otherwise return None. """ tasks = task_queue.getTasks() no_result_interactions = [] for task in tasks: if all(v == 0 for v in task.output.result): no_result_interactions.append(task.output.legend_name) # If none of the tasks had output, do not save plot if len(tasks) == len(no_result_interactions): return ( f"Cannot display plot series for any interaction plots. " f"Plot will not be created.\n{MISSING_INTERACTION_TYPES_ERROR}") elif no_result_interactions: plural = 's' if len(no_result_interactions) > 1 else '' return (f"Cannot display plot series for interaction{plural} " f"{', '.join(no_result_interactions)}.\n" f"{MISSING_INTERACTION_TYPES_ERROR}") return None
[docs] def getInitialPlotTitleAndTooltip(self): """ For multi-series plots, title will be updated when all tasks complete via if multiple ASL sets are used. """ task_mapping = defaultdict(list) tasks = self.task_queue.getTasks() for task in tasks: # Verify that output contains non-0 values and was plotted if not all(v == 0 for v in task.output.result): fit_asl = task.input.fit_asl analysis_mode = task.input.analysis_mode task_mapping[fit_asl].append(analysis_mode) if len(task_mapping) > 1: return self._getPlotTitleByMapping(task_mapping) else: task = self.getTasks()[0] interactions_mode = INTERACTION_ASL_TO_TITLE[task.input.fit_asl] # TODO: consider showing more information in a tooltip instead of # returning None return f'Interactions ({interactions_mode})', None
def _getPlotTitleByMapping(self, task_mapping): """ Return the title and tooltip for the plot based on the given mapping of ASLs to plot analysis modes. :param task_mapping: Dictionary where keys are fit ASLs and values are lists of analysis modes. Each ASL must be one of the keys for the INTERACTION_ASL_TO_TITLE constant. :type task_mapping: dict :return: Title and tooltip :rtype: str, str """ title = 'Interactions (Varied Atom Sets)' # Add span to force rich text, to get the line breaks tooltip = '<span>' for asl, modes in task_mapping.items(): recep_disp = None if asl == LIGAND_RECEPTOR_ASL: hub = maestro_ui.MaestroHub.instance() intview = hub.getInteractionViewState() if modes[0] in traj_plot_models.NON_COVALENT_MODES: recep_disp = LIGAND_RECEPTOR_DISPLAY_TO_TITLE[ intview.non_covalent_display] elif modes[0] in traj_plot_models.PI_INTERACTION_MODES: recep_disp = LIGAND_RECEPTOR_DISPLAY_TO_TITLE[ intview.pi_display] title_asl = INTERACTION_ASL_TO_TITLE[asl] mode_names = [ traj_plot_models.ANALYSIS_MODE_MAP[mode].name for mode in modes ] tooltip += f'{title_asl}: {", ".join(mode_names)}\n' if recep_disp: tooltip += f' ({recep_disp})' tooltip += '\n' tooltip += '</span>' return title, tooltip
[docs] def getTasks(self): """ Return all tasks that are managed by this plot manager. """ return self.task_queue.getTasks()
[docs] def isRunning(self): """ Return True if the plot is still generating data. """ return bool(self.task_queue._running_tasks)
[docs] def formatPlotAxes(self): """ Formats axes tick numbers and spacing depending on result data. """ results = [] for task in self.getTasks(): results += task.output.result self._formatPlotAxes(results)
[docs] def onPlotClicked(self, value): """ There is no click handler for multi-series interactions plots. """ if self.time_to_frame_map: time = value.x() frame_idx = self._getNearestFrameForTime(time) if frame_idx is not None: eid = self.entry_traj.eid self.displayFrameAndAsl.emit(traj_plot_models.EMPTY_ASL, eid, frame_idx)