Source code for schrodinger.trajectory.trajectory_gui_dir.traj_plot_gui

"""
Plot panel for the Trajectory Player.

Copyright Schrodinger, LLC. All rights reserved.
"""
import os
from collections import defaultdict
from enum import Enum

import openpyxl

from schrodinger import get_maestro
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt.QtCore import Qt
from schrodinger.trajectory.trajectory_gui_dir import banners
from schrodinger.ui import maestro_ui
from schrodinger.ui.qt import basewidgets
from schrodinger.ui.qt import filedialog
from schrodinger.ui.qt import utils as qt_utils
from schrodinger.ui.qt.appframework2 import application
from schrodinger.utils import fileutils

from . import icons_rc  # noqa: F401
from . import plots as tplots
from . import interaction_plots as iplots
from . import rmsd_plots as rplots
from . import stylesheet
from . import traj_plot_models
from . import traj_plot_ui
from . import energy_plots
from .traj_plot_models import AnalysisMode

maestro = get_maestro()
try:
    from schrodinger.application.desmond.packages import analysis
    from schrodinger.application.desmond.packages import topo
except ImportError:
    analysis = None
    topo = None

PLOT_EXISTS_ERROR = 'Plot with these parameters already exists'

TPT = tplots.TrajectoryPlotType
INTERACTION_MODE_BY_TYPE = {
    TPT.INTERACTIONS_HYDROGEN_BONDS: AnalysisMode.HydrogenBondFinder,
    TPT.INTERACTIONS_HALOGEN_BONDS: AnalysisMode.HalogenBondFinder,
    TPT.INTERACTIONS_SALT_BRIDGE: AnalysisMode.SaltBridgeFinder,
    TPT.INTERACTIONS_PI_PI: AnalysisMode.PiPiFinder,
    TPT.INTERACTIONS_CAT_PI: AnalysisMode.CatPiFinder,
}

ASL_MODE_BY_TYPE = {
    TPT.DESCRIPTORS_RADIUS_GYRATION: AnalysisMode.Gyradius,
    TPT.DESCRIPTORS_PSA: AnalysisMode.PolarSurfaceArea,
    TPT.DESCRIPTORS_SASA: AnalysisMode.SolventAccessibleSurfaceArea,
    TPT.DESCRIPTORS_MOLECULAR_SA: AnalysisMode.MolecularSurfaceArea,
}

RMSD_MODE_BY_TYPE = {
    TPT.DESCRIPTORS_RMSD: AnalysisMode.RMSD,
    TPT.DESCRIPTORS_ATOM_RMSF: AnalysisMode.AtomRMSF,
    TPT.DESCRIPTORS_RES_RMSF: AnalysisMode.ResRMSF,
}

EXCEL_TITLE_MAX_LENGTH = 32


[docs]class TrajAnalysisPlotPanel(basewidgets.MaestroDockPanel): """ Overraching panel class for displaying trajectory graphs. This class is typically launched via the Player Toolbar. Graphs are either: 1) Indexed by frame number (ex. Polar Surface Area for each frame). These are displayed in the main panel view. 2) Indexed by atom/residue (ex. Residue RMSF over all residues selected) These can be viewed in the advanced graphs section at the bottom. :ivar displayAsl: Display the asl for the corresponding entry id Signal. args are (asl, entry_id) :type displayAsl: `QtCore.pyqtSignal(str, int)` :ivar displayFrameAndAsl: Change frame and show ASL for given entry id Signal args are (asl, entry_id, frame_number) :type displayFrameAndAsl: `QtCore.pyqtSignal(str, int, int)` """ model_class = traj_plot_models.TrajPlotModel ui_module = traj_plot_ui createPlot = QtCore.pyqtSignal(Enum) trajectoryChanged = QtCore.pyqtSignal(int) displayFrameAndAsl = QtCore.pyqtSignal(str, int, int) displayAsl = QtCore.pyqtSignal(str, int)
[docs] def __init__(self, dockarea=None, **kwargs): super().__init__(object_name="trajectory_toplevel", dockarea=dockarea, **kwargs) # Style dockwidget. It does not inherit parent's stylesheet widget = self.widget() widget.setObjectName("trajectory_plot_dockwidget") widget.setStyleSheet(stylesheet.UPPER_DOCKED_WIDGET_STYLESHEET)
[docs] def initSetOptions(self): super().initSetOptions() self.setWindowTitle("Plot Computed Values Over Time") self.setObjectName('trajectory_plot_panel') self.setStyleSheet(stylesheet.TRAJECTORY_PLOT_GUI_STYLESHEET)
[docs] def initSetUp(self): super().initSetUp() self.entry_traj = None self.interactions_use_visible_atoms = True self.ui.reset_btn.clicked.connect(self._onResetButtonClicked) self.ui.export_results_btn.clicked.connect(self._onExportResultsClicked) self.ui.save_definitions_btn.clicked.connect( self._onSaveDefinitionsClicked) # Hidden, see PANEL-15665 self.ui.save_definitions_btn.hide() self._bannerman = qt_utils.MaestroPythonBannerManager() self.planar_banner = banners.PlanarBanner() self.planar_banner.planarAnglesPicked.connect( self._createPlanarAnglePlot) self.planar_banner.closeBanner.connect(self._bannerman.closeBanner) self._bannerman.modalBannerClosed.connect( self.planar_banner.stopPicking) self.centroid_banner = banners.CentroidBanner() self.centroid_banner.centroidsPicked.connect( self._createCentroidDistancePlot) self.centroid_banner.closeBanner.connect(self._bannerman.closeBanner) self._bannerman.modalBannerClosed.connect( self.centroid_banner.stopPicking) self.rmsdDlg = rplots.RMSDDialog() self.job_settings_dlg = energy_plots.EnergyJobSettingsDialog() self.trajectoryChanged.connect(self.rmsdDlg.trajectoryChanged) self.current_frame_idx = 0 self._initPlotsMap() if maestro: maestro.project_close_callback_add(self._onProjectClosed) # Update label showing number of plots being generated every second: self.periodic_timer = QtCore.QTimer() self.periodic_timer.timeout.connect(self._updateStatusLabel) self.periodic_timer.setInterval(1000)
[docs] def initLayout(self): super().initLayout()
[docs] def getSignalsAndSlots(self, model): return [ (self.createPlot, self.createTrajectoryPlot), (self.trajectoryChanged, self.rmsdDlg.trajectoryChanged) ] # yapf: disable
def _initPlotsMap(self): """ Initialize the plots map. """ self.plot_managers = [] self.plot_widgets = []
[docs] def event(self, event): """ Make sure that scrolling doesn't cause Workspace zoom. See Qt documentation for argument documentation. """ if isinstance(event, QtGui.QWheelEvent): event.setAccepted(True) return True return super().event(event)
def _onProjectClosed(self): """ Handle project close events """ try: self._resetPanel() except RuntimeError: # Can happen if Maestro is being closed pass def _resetPanel(self): """ Delete all current plots. """ while self.plot_managers: self.removePlot(self.plot_managers.pop()) self._removeShortcutRows() def _removeShortcutRows(self): """ Removes the shortcut rows in the Advanced Plot portion of the panel. Each ShortcutRow instance contains one or more plot widgets. """ while self.ui.advanced_plots_layout.count(): wdg = self.ui.advanced_plots_layout.takeAt(0).widget() if wdg is not None: wdg.setVisible(False) wdg.deleteLater() def _onResetButtonClicked(self): """ Open the Reset Button dialog """ msg = ("Are you sure you want to reset the panel? This will remove " "all graphs.") response = self.question(msg) if response is True: self._resetPanel() def _onSaveDefinitionsClicked(self): """ Open the Save Definitions dialog """ pass
[docs] def plotAlreadyExists(self, settings_hash): """ Check if a plot already exists. :param settings_hash: Settings string uniquely identifying a plot. :type settings_hash: str :return: True if the specified plot already exists, False otherwise. :rtype: bool """ for plot_manager in self.plot_managers: if plot_manager.settings_hash == settings_hash: return True return False
def _getCentroidsForMeasurement(self, measurement, centroids_map): """ Get a list of lists of atom numbers that each define a centroid for the specified measurement. :param measurement: Tuple defining the measurement :type measurement: tuple :param centroids_map: Dictionary of centroid definition mappings. :type centroids_map: dict :return: List of lists of atom IDs that define the centroids :rtype: list(list) """ # Final value of the measurement tuple is the measurement itself xids = measurement[:-1] centroids = [] for xid in xids: if xid.startswith('RCEN'): aids = centroids_map[xid] else: aids = [int(xid.split(':')[-1])] centroids.append(aids) return centroids
[docs] def validateTrajectory(self): """ Return True if there is a trajectory loaded and it is included in the Workspace; False otherwise. """ eids = maestro.get_included_entry_ids() if len(eids) != 1: self.error( "Please ensure a single entry is included in the Workspace") return False if not self.entry_traj: self.warning("No trajectory currently loaded.") return False if str(self.entry_traj.eid) not in eids: self.error( "Please ensure that the loaded trajectory entry is included in the Workspace" ) return False return True
[docs] def createTrajectoryPlot(self, plot_type): """ Starts the creation of a plot to be displayed in the panel. Shows the panel. :param plot_type: Type of plot for the panel to create and display :type: PlotType """ self.show() if not self.validateTrajectory(): return TPT = tplots.TrajectoryPlotType if plot_type == TPT.MEASUREMENT_WORKSPACE: self._createWSMeasurementsPlots() elif plot_type == TPT.MEASUREMENT_ADD: # TODO - Implement this functionality pass elif plot_type == TPT.MEASUREMENT_PLANAR_ANGLE: self._bannerman.showBanner(self.planar_banner) self.planar_banner.startPicking() elif plot_type == TPT.MEASUREMENT_CENTROID: # NOTE: Disabled as part of PANEL-20518 self._bannerman.showBanner(self.centroid_banner) self.centroid_banner.startPicking() elif plot_type == TPT.INTERACTIONS_ALL: self._createAllInteractionsPlot() elif plot_type in INTERACTION_MODE_BY_TYPE: mode = INTERACTION_MODE_BY_TYPE[plot_type] self.createPlotManager(iplots.InteractionsPlotManager, mode, self.interactions_use_visible_atoms) elif plot_type in ASL_MODE_BY_TYPE: mode = ASL_MODE_BY_TYPE[plot_type] self._createASLPlot(mode) elif plot_type in RMSD_MODE_BY_TYPE: mode = RMSD_MODE_BY_TYPE[plot_type] self._createRMSPlot(mode) elif plot_type in tplots.ENERGY_PLOT_TYPES: self._createEnergyPlot(plot_type) else: self.warning("Unknown plot type: %s" % plot_type)
[docs] def createPlotManager(self, plot_cls, *args, **kwargs): """ Create a plot manager of given class, with specified options, and start the plot generation. Shows a warning and does nothing if a plot with same settings already exists. Warning is also shown if plot fails to be created or started. :param plot_cls: Class of the plot to create :type plot_cls: AbstractTrajectoryPlotManager """ try: plot_manager = plot_cls(self, *args, **kwargs) except RuntimeError as err: self.showPlotWarning(str(err)) return plot_manager.settings_hash = plot_manager.getSettingsHash() if self.plotAlreadyExists(plot_manager.settings_hash): self.warning(PLOT_EXISTS_ERROR) return plot_manager.plot_number = self.getNextAvailablePlotNumber(plot_manager) self.setupPlotSignals(plot_manager) plot_manager.start() self.plot_managers.append(plot_manager) self._updateStatusLabel() return plot_manager
[docs] def setupPlotSignals(self, plot_manager): """ Hook up plot manager's signals to panel's slots. """ plot_manager.displayFrameAndAsl.connect( self._onDisplayFrameAndAslRequested) plot_manager.displayAsl.connect(self._onDisplayAslRequested) plot_manager.showWarning.connect(self.showPlotWarning) plot_manager.newDataAvailable.connect( lambda: self.onNewPlotDataAvailable(plot_manager))
[docs] def getNextAvailablePlotNumber(self, pmanager): """ Return the plot number that should be used for next plot of given type. For "All Interactions" plots, the plot number is currently not used. :param pmanager: Plot manager instance :type pgmanager: AbstractTrajectoryPlotManager """ def get_type(pm): # Given a plot manager, returns a string that is shared by all plots # of the same type when determining the next available plot number. if isinstance(pm, iplots.MultiSeriesPlotManager): return 'All Interactions' plot_type = pm.getPlotType() if plot_type == tplots.PlotDataType.RMSF: return 'RMSF' if plot_type == tplots.PlotDataType.ENERGY: return 'Energy' else: return pm.task.input.analysis_mode ptype = get_type(pmanager) used_plot_numbers = [ pm.plot_number for pm in self.plot_managers if get_type(pm) == ptype ] if used_plot_numbers: return max(used_plot_numbers) + 1 else: return 1
def _createWSMeasurementsPlots(self): """ Create plots for all measurements currently in the Workspace. """ maestro_hub = maestro_ui.MaestroHub.instance() distances = maestro_hub.getDistanceMeasurements() angles = maestro_hub.getAngleMeasurements() dihedrals = maestro_hub.getDihedralMeasurements() centroids_map = maestro_hub.getRingCentroids() if not distances and not angles and not dihedrals: return for measurement in distances + angles + dihedrals: centroids = self._getCentroidsForMeasurement( measurement, centroids_map) centroid_asls = [ 'atom.n ' + ' '.join(list(map(str, c))) for c in centroids ] num_centroids = len(centroids) if num_centroids == 2: mode = AnalysisMode.Distance elif num_centroids == 3: mode = AnalysisMode.Angle else: mode = AnalysisMode.Torsion self.createPlotManager(tplots.WorkspaceMeasurementPlotManager, mode, measurement, centroid_asls) def _createPlanarAnglePlot(self, alist): """ Create a planar angle plot. :param alist: List of 6 atoms making up the 2 angles :type alist: list """ if not self.validateTrajectory(): return self.createPlotManager(tplots.PlanarAngleAnalysisPlot, alist) def _createCentroidDistancePlot(self, atom_sets): """ Create a distance plot of centroids. NOTE: Disabled as of PANEL-20518 :param atom_sets: List of tuples of atom IDs constituting the first set. :type atom_sets: list(tuple(int)) """ if not self.validateTrajectory(): return self.createPlotManager(tplots.CentroidDistancePlotManager, atom_sets) def _createASLPlot(self, analysis_mode): """ Create an ASL plot of the given mode, set up with the currently selected Workspace atoms. """ asl = maestro.selected_atoms_get_asl() anums = maestro.selected_atoms_get() self.createPlotManager(tplots.AslPlotManager, analysis_mode, asl, anums)
[docs] def updateFsysCt(self, frame_number): """ Updates the entry trajectory's fsys_ct structure coordinates at the frame provided :param frame_number: frame number of trajectory :type frame_number: int :return: A copy of the frame CT :rtype: cms.Cms """ cms_model = self.entry_traj.cms_model frame = self.entry_traj.trajectory[frame_number] topo.update_fsys_ct_from_frame_GF(cms_model.fsys_ct, cms_model, frame) return cms_model
[docs] def getRmsdReferenceCt(self): """ Return fsys_ct structure at the frame that user selected to use for RMSD reference. :return: A copy of the frame CT :rtype: cms.Cms """ if self.rmsdDlg.ui.frame_rb.isChecked(): ref_frame_idx = self.rmsdDlg.getFrameNumber() - 1 else: ref_frame_idx = self.current_frame_idx return self.updateFsysCt(ref_frame_idx)
def _createRMSPlot(self, mode): """ Create a new RMS plot (RMSD, AtomRMSF, and ResRMSF modes) :param mode: type of RMS analysis to perform :type mode: AnalysisMode """ # Must convert tuple to list in order to serialize param: aids = list(maestro.selected_atoms_get()) # Get the AIDs and reference coordinates for each, based on RMSD dialog # reference selection: try: fit_aids, fit_ref_pos = self.rmsdDlg._calculateFitAttributes( self.entry_traj) except RuntimeError as err: self.warning(str(err)) return # CMS model, with reference frame selected: cms_model = self.getRmsdReferenceCt() if mode == AnalysisMode.AtomRMSF: plot_cls = rplots.AtomRmsfPlotManager elif mode == AnalysisMode.ResRMSF: plot_cls = rplots.ResidueRmsfPlotManager elif mode == AnalysisMode.RMSD: plot_cls = rplots.RmsdByFramePlotManager else: raise ValueError("Non-RMSF analysis mode: %s" % mode) self.createPlotManager(plot_cls, cms_model, aids, fit_aids, fit_ref_pos) def _createEnergyPlot(self, plot_type): """ Create energy plot of given type. """ if not self.job_settings_dlg.isDoNotShowChecked(): self.job_settings_dlg.showDialog(show_cb=True) st = self.entry_traj.cms_model.fsys_ct TPT = tplots.TrajectoryPlotType if plot_type == TPT.ENERGY_ALL_GROUPED: sets = energy_plots.find_all_grouped_sets(st) # There are only 9 types of set types that are detectable, so the # above function should never return more than 10. assert len( sets ) <= 10, "find_all_grouped_sets() returned more than 10 sets" self.createEnergyPlotForSets(sets) elif plot_type == TPT.ENERGY_ALL_INDIVIDUAL: sets = energy_plots.find_all_individual_sets(st) if len(sets) > 10: self.warning( "Maximum of 10 substructure sets are supported with this option (%i found)" % len(sets)) self.createEnergyPlotForSets(sets) elif plot_type == TPT.ENERGY_INDIVIDUAL_MOLECULES: sets = energy_plots.find_sets_for_all_molecules(st) if len(sets) > 10: self.warning( 'Maximum of 10 molecules are supported with this option (%i found)' % len(sets)) return self.createEnergyPlotForSets(sets) elif plot_type == TPT.ENERGY_CUSTOM_SUBSTRUCTURE_SETS: dialog = energy_plots.CustomSubstructureSetsDialog(st, self) dialog.setWindowFlag(Qt.WindowStaysOnTopHint) dialog.setsDefined.connect(self.createEnergyPlotForSets) dialog.show() elif plot_type == TPT.ENERGY_CUSTOM_ASL_SETS: dialog = energy_plots.CustomAslSetsDialog(st, self) dialog.setWindowFlag(Qt.WindowStaysOnTopHint) dialog.setsDefined.connect(self.createEnergyPlotForSets) dialog.show() def _createAllInteractionsPlot(self): self.createPlotManager(iplots.MultiSeriesPlotManager, self.interactions_use_visible_atoms)
[docs] def createEnergyPlotForSets(self, sets): """ Submit a TrajectoryEnergyJobTask for given sets. A plot window will be created when the job completes. :param sets: Dict where keys are set names and values are ASLs. :type sets: dict """ if not self.validateTrajectory(): return cms_fpath, _ = self.entry_traj.getCMSPathAndTrajDir() base = fileutils.strip_extension(cms_fpath)[:-len('-out')] cfg_file = base + '-out.cfg' if not os.path.isfile(cfg_file): cfg_file = energy_plots.BrowseCfgFileDialog( cfg_file).askForNewCfgFile() if not cfg_file: # cancel pressed return return self.createPlotManager(tplots.EnergyPlotManager, cfg_file, sets)
[docs] def showRmsdSettingsDialog(self): """ Shows the RMSD Settings Dialog """ self.rmsdDlg.show()
[docs] def showEnergySettingsDialog(self): """ Shows energy job settings dialog in modal mode. Called from "Job Settings..." menu item. """ self.job_settings_dlg.showDialog(show_cb=False)
def _onDisplayFrameAndAslRequested(self, fit_asl, eid, frame_idx): """ Load the specified frame into the Workspace and display asl. :param fit_asl: ASL to fit the Workspace view to :type fit_asl: str :param eid: Entry ID of the trajectory associated with the plot :type eid: int :param frame_idx: Frame to be loaded :type frame_idx: int """ self.displayFrameAndAsl.emit(fit_asl, eid, frame_idx) def _onDisplayAslRequested(self, fit_asl, eid): """ Propogates the signal for fitting asl in the Workspace. :param fit_asl: ASL to fit the Workspace view to :type fit_asl: str :param eid: Entry ID of the trajectory associated with the plot :type eid: int """ self.displayAsl.emit(fit_asl, eid)
[docs] def onNewPlotDataAvailable(self, plot_manager): """ Called when the given plot manager has finished creating the plot. For multi-series plots, it gets called for every series that is done. """ # Update the label showing the number of active tasks: self._updateStatusLabel() # If there is no plot widget present (this is the first task that # has completed), create one, and insert it into main panel: for plot_widget in self.plot_widgets: if plot_widget.plot == plot_manager: return plot_widget = self.createPlotWidget(plot_manager) self.plot_widgets.append(plot_widget) self.repaint()
[docs] def createPlotWidget(self, plot_manager): """ Create a new plot widget for displaying the given plot. For simple plots, a CollapsiblePlot is created. For advanced plots, a plot panel with AdvancedPlotShortcut is created. :param plot: Plot object containing the chart. :type plot: AbstractTrajectoryPlotManager :return: Plot widget. :rtype: tplots.CollapsiblePlot or tplots.AdvancedPlotShortcut """ plot_panel = plot_manager.createPanel() if plot_panel: # "advanced" plot. Shortcut widget opens plot panel when clicked. # Currently only "RMSF" and "Energy" plots have panels. plot_widget = plot_manager.createShortcutWidget(plot_panel) self._insertShortcut(plot_widget) else: # Simple plot (no plot panel; plot is inserted into main panel) plot_widget = plot_manager.createCollapsiblePlotWidget() self.ui.plots_layout.insertWidget(-1, plot_widget) plot_manager.deletePlot.connect(lambda: self.removePlot(plot_manager)) return plot_widget
def _insertShortcut(self, wdg): """ Inserts the shortcut widget into the advanced plot layout :param wdg: Shortcut widget :type wdg: AdvancedPlotShortcut """ parent_layout = self.ui.advanced_plots_layout inserted = False for idx in range(parent_layout.count()): row = parent_layout.itemAt(idx).widget() if row.hasSpace(): row.addWidget(wdg) inserted = True if not inserted: row = tplots.ShortcutRow() row.addWidget(wdg) parent_layout.insertWidget(-1, row) def _getShortcutCount(self): layout = self.ui.advanced_plots_layout count = 0 for idx in range(layout.count()): row = layout.itemAt(idx).widget() count += row.widgetCount() return count
[docs] def countPlotsOfType(self, mode): """ Count the number of plots with the given type that are present. """ num_plots = 0 for plot_manager in self.plot_managers: if plot_manager.task.input.analysis_mode == mode: num_plots += 1 return num_plots
[docs] def removePlot(self, plot_manager): """ Remove the given plot manager from the panel. If it has a plot widget associated with it, it will also be removed. :param plot_manager: Reference to the plot manager. :type plot_manager: AbstractTrajectoryPlotManager """ for plot_widget in self.plot_widgets: if plot_widget.plot == plot_manager: plot_widget.layout().removeWidget(plot_widget) self.plot_widgets.remove(plot_widget) plot_widget.close() plot_widget.deleteLater() break if plot_manager in self.plot_managers: self.plot_managers.remove(plot_manager) self.repaint()
[docs] def addWorkspaceMeasurements(self): """ Opens the workspace measurement banner, allowing the user to add measurements """ self._bannerman.closeBanner() maestro_hub = maestro_ui.MaestroHub.instance() maestro_hub.emitCreateMeasurementBanner()
[docs] def getPlotCount(self): """ :return: The current plots count. Used by playertoolbar.py. :rtype: int """ return len(self.plot_managers)
def _onExportResultsClicked(self): """ Export results for all current plots to an Excel spreadsheet. """ # List of 2-tuples containing worksheet title and data sheets = self._generateExportData() if not sheets: self.warning('No data to export') return fname = filedialog.get_save_file_name(parent=self, caption="Save as Excel Workbook", filter="Excel (*.xlsx)") if not fname: return wb = openpyxl.Workbook() wb.remove(wb['Sheet'] ) # excel workbook default Sheet needs to be renamed/removed. for ws_idx, (sheet_title, data) in enumerate(sheets): if len(sheet_title) >= EXCEL_TITLE_MAX_LENGTH: sheet_title = sheet_title[:EXCEL_TITLE_MAX_LENGTH - 1] current_ws = wb.create_sheet(title=sheet_title, index=ws_idx) for row in data: current_ws.append(row) wb.save(fname) def _generateExportData(self): """ Return data for all trajectory, RMSF, and Energy plots, as a list of (sheet name, list of data). :return: Data for all tables to be exported. :rtype: list((str, list(list)) """ # Assemble export data, grouped by cms and separated by respective plot type. traj_export_data = defaultdict(list) rmsf_export_data = defaultdict(list) energy_export_data = defaultdict(list) for plot_widget in self.plot_widgets: plot = plot_widget.plot # NOTE: For multi-series interactions plots, the plot may not # have completed all series yet - see PANEL-20174 cms_title = plot.entry_traj.cms_model.title plot_type = plot.getPlotType() export_data = plot.getExportData() if plot_type == tplots.PlotDataType.TRAJECTORY: group_dict = traj_export_data # All trajectory plots go on the same sheet plot_title = 'Trajectory' elif plot_type == tplots.PlotDataType.RMSF: group_dict = rmsf_export_data plot_title = plot_widget.getPlotTitle() elif plot_type == tplots.PlotDataType.ENERGY: group_dict = energy_export_data plot_title = plot_widget.getPlotTitle() sheet_title = f'{cms_title} {plot_title}' group_dict[(plot.cms_fpath, sheet_title)].append(export_data) # TODO factor out duplication with above sheets = [] fmt_traj_export_data = self._formatTrajectoryExportData( traj_export_data) # Trajectory plot sheet for (_, sheet_title), current_data in fmt_traj_export_data.items(): sheets.append((sheet_title, current_data)) # One sheet for each advanced_exports = [rmsf_export_data, energy_export_data] for advanced_export_data in advanced_exports: for (_, sheet_title), cms_plot_data in advanced_export_data.items(): for current_data in cms_plot_data: sheets.append((sheet_title, current_data)) return sheets def _formatTrajectoryExportData(self, export_data): """ Format trajectory export data by combining data of same cms filename. """ fmt_traj_export_data = defaultdict(list) for cms_fname, cms_plot_data in export_data.items(): for plot_idx, current_data in enumerate(cms_plot_data): formatted_ws_data = fmt_traj_export_data[cms_fname] for row_idx, current_row in enumerate(current_data): # Strip off redundant timestamp and frame index if plot_idx == 0: formatted_ws_data.append(current_row) else: formatted_ws_data[row_idx].extend(current_row[2:]) return fmt_traj_export_data
[docs] def deletePlotsForEntry(self, eid): """ Remove plots associated with the specified entry id. :param eid: Entry ID to remove the plots of. :type eid: int """ for plot in self.plot_managers: if plot.entry_traj.eid == eid: self.removePlot(plot)
def _updateStatusLabel(self): """ Update the status label based on the currently running tasks. :param starting_task: Optional task that has just been started. :type task: traj_plot_models.TrajectoryAnalysisSubprocTask or traj_plot_models.TrajectoryEnergyJobTask """ num_running = sum([1 for pm in self.plot_managers if pm.isRunning()]) plural = 's' if num_running > 1 else '' msg = '' if num_running == 0 else f"{num_running} plot{plural} being generated..." self.ui.status_label.setText(f'<font color=#e8e8e8>{msg}</font>')
[docs] def onCurrentFrameChanged(self, frame): self.current_frame_idx = frame - 1
[docs] def showPlotWarning(self, msg): """ Show the given text in a warning dialog, with dialog title set to "Cannot Display Plot" instead of the standard "Warning" title. """ self.warning(msg, title="Cannot Display Plot")
panel = TrajAnalysisPlotPanel.panel if __name__ == "__main__": application.start_application(panel)