"""
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()
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)