Source code for schrodinger.application.canvas.clustergui

"""

Canvas clustering functionality that uses GUI libraries

There are classes to perform custering and to support
graphical interfaces to the clustering options.

Copyright Schrodinger, LLC. All rights reserved.

"""

# Contributors: Quentin McDonald

import csv

import numpy
from matplotlib import cm
from matplotlib.patches import Rectangle
from matplotlib.widgets import Cursor

import schrodinger.application.canvas.cluster as cluster
import schrodinger.ui.qt.appframework as appframework
import schrodinger.ui.qt.structure2d as structure2d
import schrodinger.ui.qt.swidgets as swidgets
from schrodinger.infra import canvas2d
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.ui.qt import smatplotlib
from schrodinger.utils import csv_unicode

try:
    from schrodinger import maestro
except ImportError:
    maestro = None  # Running outside of Maestro

# PyQt GUI Classes start here ###########################


[docs]class CanvasFingerprintClusterGUI(cluster.CanvasFingerprintCluster): """ A subclass of the canvas fingerprint clusterer which is to be used from a program with a TKInter interface. This class has methods for creating a component which displays the clustering linkage options """ CREATE_RADIOBUTTON_NAMES = [ "Duplicate entries to a new group for each cluster", "Move entries to a new group for each cluster", "A group containing the structures nearest the centroid in each cluster", "Cluster index and size properties for each entry" ] DUPLICATE, MOVE, REPRESENTATIVE, ENTRY = list(range(4))
[docs] def __init__(self, logger): super(CanvasFingerprintClusterGUI, self).__init__(logger) self.plot_dialog = None self.dendrogram_dialog = None self.num_clusters_entry = None self.distance_matrix_dialog = None self.distance_matrix_callback = None self.times_applied = 0
[docs] def getTab(self, command=None, linkage=True, cluster=True, results=True, apply=True, msg=None): """ Creates a tab that can be used in a QTabWidget for calculating clustering. The tab has the following sections: Linkage, Cluster, Clustering Results, Apply Clustering :type command: callable object :param command: function to be called when the Calculate Clustering button is pressed. :type linkage: bool :param linkage: True if this section should be included (default) :type cluster: bool :param cluster: True if this section should be included (default) :type results: bool :param results: True if this section should be included (default) :type apply: bool :param apply: True if this section should be included (default) :type msg: str :param msg: The message that appears right above the Calculate Clustering button. :rtype: QWidget :return: widget containing the clustering gui Usage: QTabWidget.addTab(fp_sim.getTab(doClustering)) """ widget = QtWidgets.QWidget() tab_layout = swidgets.SVBoxLayout(widget) if linkage: self.linkage_gui = self.getLinkageGUI() tab_layout.addWidget(self.linkage_gui) if cluster: self.cluster_gui = self.getClusterGUI(command, msg=msg) tab_layout.addWidget(self.cluster_gui) if results: self.results_gui = self.getResultsGUI() tab_layout.addWidget(self.results_gui) if apply: self.apply_gui = self.getApplyGUI() tab_layout.addWidget(self.apply_gui) tab_layout.addStretch() return widget
[docs] def getClusterGUI(self, command, msg=None): """ Returns a GUI Group Box which displays the Cluster Calculation button :type msg: str :param msg: The message that appears right above the Calculate Clustering button. :type command: callable object :param command: function to be called when the Calculate Clustering button is pressed. :rtype: swidgets.SGroupBox (QGroupBox) :return: groupbox containing the cluster widgets """ self.cluster_group = swidgets.SGroupBox('Cluster') if msg is None: msg = 'Clustering is performed on the selected entries in the ' + \ 'project\nusing the current fingerprint and similarity' + \ ' settings.' self.cluster_group.layout.addWidget(QtWidgets.QLabel(msg)) swidgets.SPushButton('Calculate Clustering', command=command, layout=self.cluster_group.layout) return self.cluster_group
[docs] def getLinkageGUI(self): """ Returns a GUI component which displays the cluster linkage options :rtype: swidgets.SGroupBox (QGroupBox) :return: groupbox containing the linkage widgets """ # Create the widgets self.base_group = swidgets.SGroupBox('Linkage', layout='horizontal') self._linkage_label = QtWidgets.QLabel('Linkage method:') self.base_group.layout.addWidget(self._linkage_label) # Put the methods into the combobox and select 'Average' as the default self.linkage_combobox = swidgets.SComboBox( items=self.LINKAGE_TYPES, default_item='Average', command=self.setLinkage, layout=self.base_group.layout) self.base_group.layout.addStretch() return self.base_group
[docs] def getResultsGUI(self): """ Returns a GUI which displays the clustering results in terms of the strain and best number of clusters and provides access to plots for the dendrogram and cluster statistics. This will be deactivated until the update() method is called at which time it will be activated and refreshed with the results from the most recent clustering """ self.results_base_group = swidgets.SGroupBox('Clustering Results') # Create and lay out the labels self.strain_label = QtWidgets.QLabel('Cluster strain is:') self.best_clu_label = QtWidgets.QLabel('Best number of clusters is:') self.results_base_group.layout.addWidget(self.strain_label) self.results_base_group.layout.addWidget(self.best_clu_label) # Create and lay out the buttons self._button_layout = swidgets.SHBoxLayout() self._clustering_button = swidgets.SPushButton( 'Clustering statistics...', command=self.showStatisticsPlot) self._dendrogram_button = swidgets.SPushButton( 'Dendrogram...', command=self.showDendrogramPlot) self._distance_button = swidgets.SPushButton( 'Distance matrix...', command=self.showDistanceMatrixPlot) self._button_layout.addWidget(self._clustering_button) self._button_layout.addWidget(self._dendrogram_button) self._button_layout.addWidget(self._distance_button) self._button_layout.addStretch() self.results_base_group.layout.addLayout(self._button_layout) # Start with all results disabled: self.results_base_group.setEnabled(False) return self.results_base_group
[docs] def setNumClusters(self, num): """ Set the number of clusters in the Apply Clustering Section """ self.apply_num_edit.setText(str(num))
[docs] def getApplyGUI(self): """ Return the controls used to apply the clustering to selected entries in the project table """ self.apply_clu_group = swidgets.SGroupBox('Apply Clustering') self._apply_num_layout = swidgets.SHBoxLayout() self._apply_button_layout = swidgets.SHBoxLayout() self.apply_clu_group.layout.addLayout(self._apply_num_layout) num_label = QtWidgets.QLabel('Number of clusters:') # A QLineEdit that isn't too wide and only accepts positive ints self.apply_num_edit = QtWidgets.QLineEdit('1') self.apply_num_edit.setMaximumWidth(60) posint_validator = QtGui.QIntValidator(self.apply_num_edit) posint_validator.setBottom(1) self.apply_num_edit.setValidator(posint_validator) self._apply_num_layout.addWidget(num_label) self._apply_num_layout.addWidget(self.apply_num_edit) self._apply_num_layout.addStretch() create_label = QtWidgets.QLabel('Create:') self.apply_clu_group.layout.addWidget(create_label) # Now radio buttons, with the first one selected self.apply_radio_buttons = [] for astring in self.CREATE_RADIOBUTTON_NAMES: self.apply_radio_buttons.append(QtWidgets.QRadioButton(astring)) self.apply_clu_group.layout.addWidget(self.apply_radio_buttons[-1]) self.apply_radio_buttons[0].setChecked(True) # Finally the apply button self._apply_button = swidgets.SPushButton( 'Apply Clustering', command=self.doApplyClustering) self._apply_button_layout.addWidget(self._apply_button) self._apply_button_layout.addStretch() self.apply_clu_group.layout.addLayout(self._apply_button_layout) # Start with apply disabled: self.apply_clu_group.setEnabled(False) return self.apply_clu_group
[docs] def doApplyClustering(self): """ Once the clustering has been performed for the selected entries this method will apply it """ pt = maestro.project_table_get() try: num_clusters = int(self.apply_num_edit.text()) except ValueError: # This happens for blank edits maestro.warning("Please specify the number of clusters") return # Calculate the groupings with this number of clusters: self.group(num_clusters) # Apply the clustering via the cluster maps: cluster_map = self.getClusteringMap() cluster_contents = self.getClusterContents() ids = "" for cluster in list(cluster_contents): for c in cluster_contents[cluster]: ids += " %s" % c # Select the input entries: maestro.command("entryselectonly entry_id %s" % ids) if num_clusters > len(pt.selected_rows): maestro.warning("%s %d %s %d %s" % ("It is not possible to generate", num_clusters, "clusters when there are only", len(pt.selected_rows), "entries to cluster.")) return # Figure out which option the user selected for on_button, button in enumerate(self.apply_radio_buttons): if button.isChecked(): break # For each entry add the properties: for entry in list(cluster_map): _add_cluster_statistics(entry, self, None, pt[entry]) cluster_index = cluster_map[entry] pt[entry]['i_canvas_Canvas_Cluster_Index'] = int(cluster_index) pt[entry]['i_canvas_Canvas_Cluster_Size'] = \ len(cluster_contents[cluster_index]) if on_button == self.DUPLICATE: # Entry for each group, but not disrupting any existing ones: # Duplicate the entries in place: maestro.command("entryduplicate") # Create a group for each entry: for cluster in list(cluster_contents): ids = ", ".join(cluster_contents[cluster]) # We need to create unique group names. Check the # proposed name is unique and add an increment until it is: if self.times_applied == 0: gname = "Cluster %d" % int(cluster) else: gname = "Cluster %d_%d" % (int(cluster), self.times_applied) cmd = 'entrygroupcreate "%s" entry_id %s' % (gname, ids) maestro.command(cmd) self.times_applied += 1 # Clear the properties added for the original selected entries: pnames = [ "r_canvas_Distance_To_Centroid", "b_canvas_Is_Nearest_To_Centroid", "b_canvas_Is_Farthest_From_Centroid", "r_canvas_Max_Distance_From_Centroid", "r_canvas_Average_Distance_From_Centroid", "r_canvas_Cluster_Variance", "i_canvas_Canvas_Cluster_Index", "i_canvas_Canvas_Cluster_Size" ] for p in pnames: maestro.command('propertyclearvalue allentries=selected "%s"' % p) elif on_button == self.MOVE: # Create a group for each entry: for cluster in list(cluster_contents): ids = ", ".join(cluster_contents[cluster]) if self.times_applied == 0: cmd = 'entrygroupcreate "Cluster %d" entry_id %s' % ( int(cluster), ids) else: cmd = 'entrygroupcreate "Cluster %d_%d" entry_id %s' % ( int(cluster), self.times_applied, ids) maestro.command(cmd) self.times_applied += 1 # Delete any empty groups which are left: to_delete = [] for g in pt.groups: if len(g.all_rows) == 0: to_delete.append(g.name) for d in to_delete: maestro.command('entrygroupdelete "%s" isexp=false' % d) elif on_button == self.REPRESENTATIVE: # Create a group with a single structure from each entry in it # Note do this as a dictionary as we only want a single # entry per cluster. Becase there can be more than one # entry with "nearesttocentroid" marked we choose only the first: repr_entries = {} for entry in list(cluster_map): if self.getIsNearestToCentroid(entry): cluster_index = int(cluster_map[entry]) if cluster_index not in repr_entries: repr_entries[cluster_index] = entry ids = ", ".join(list(repr_entries.values())) # Duplicate the representative entries: maestro.command("entryduplicate entry_id %s" % ids) # This causes the duplicate entries to be selected # Move them to a new group: group_name = "Representative Entries" if self.times_applied > 0: group_name += " %d" % self.times_applied maestro.command('entrygroupcreate "%s" selected' % group_name) self.times_applied += 1 pt.update()
[docs] def updateResults(self): """ Once clustering has been performed this method should be called to update the clustering results GUI: """ # Enable the results and apply groups: self.results_base_group.setEnabled(True) self.apply_clu_group.setEnabled(True) self.strain_label.setText('Clustering strain is: %5.3f' % self._strain) self.best_clu_label.setText('Best number of clusters is: %d' % self.getBestNumberOfClusters()) # Update the plots if they are visible: if self.plot_dialog: self.plot_dialog.redraw() if self.dendrogram_dialog: self.dendrogram_dialog.redraw() if self.distance_matrix_dialog: self.distance_matrix_dialog.redraw()
[docs] def close(self): """ Perform the tasks necessary when closing the panel. This will include closing all the open plot windows """ if self.plot_dialog: self.plot_dialog.close() if self.dendrogram_dialog: self.dendrogram_dialog.close() if self.distance_matrix_dialog: self.distance_matrix_dialog.close()
[docs] def showStatisticsPlot(self): """ Display a plot of clustering statistics """ if not self.plot_dialog: self.plot_dialog = ClusterStatisticsPlotDialog(self) self.plot_dialog.num_clusters_selected.connect(self.setNumClusters) self.plot_dialog.show() return
[docs] def showDendrogramPlot(self): """ Display the clustering dendgoram """ if not self.dendrogram_dialog: self.dendrogram_dialog = DendrogramPlotDialog(self) self.dendrogram_dialog.num_clusters_selected.connect( self.setNumClusters) self.dendrogram_dialog.show() return
[docs] def showDistanceMatrixPlot(self): """ Display the distance matrix """ if not self.distance_matrix_dialog: self.distance_matrix_dialog = DistanceMatrixPlotDialog( self, self.apply_num_edit, self.distance_matrix_callback) self.distance_matrix_dialog.show() return
[docs] def setDistanceMatrixCallback(self, cb): """ Set a callback to be called when the mouse is clicked in the distance matrix plot. This should expect to receive two entry IDs """ self.distance_matrix_callback = cb
[docs] def getClusteringStatistics(self, id): return { 'r_canvas_Distance_To_Centroid': self.getDistanceToCentroid(id), 'b_canvas_Is_Nearest_To_Centroid': self.getIsNearestToCentroid(id), 'b_canvas_Is_Farthest_From_Centroid': self.getIsFarthestFromCentroid(id), 'r_canvas_Max_Distance_From_Centroid': self.getMaxDistanceFromCentroid(id), 'r_canvas_Average_Distance_From_Centroid': self.getAverageDistanceFromCentroid(id), 'r_canvas_Cluster_Variance': self.getClusterVariance(id), }
# TODO: Port to AppFramework2, factor out common code with DendrogramPlotDialog
[docs]class ClusterStatisticsPlotDialog(appframework.AppFramework): """ A class which displays a dialog with a plot of the statistics for the most recent clustering """ num_clusters_selected = QtCore.pyqtSignal(int) PLOT_TYPES = [ "Kelley Penalty", "R-Squared", "Semipartial R-Squared", "Merge Distance", "Separation Ratio" ]
[docs] def __init__(self, canvas_cluster): """ Create an instance of the dialog. Objects passed are the parent and the CanvasFingerprintCluster object which will have the statistics :type canvas_cluster: CanvasFingerprintCluster object :param canvas_cluster: object that contains the clustering statistics """ # Store arguments self.fp_clu = canvas_cluster self.visible = False # Create the window buttons = {'close': {'command': self.close}} appframework.AppFramework.__init__(self, buttons=buttons, title='Clustering Statistics', subwindow=True) # Create the plot self.canvas = smatplotlib.SmatplotlibCanvas(width=5, height=5, layout=self.interior_layout) self.sub_plot = self.canvas.figure.add_subplot(111) self.canvas.show() # The control frame widgets self._ctrl_layout = swidgets.SHBoxLayout() self._ctrl_label = QtWidgets.QLabel('Plot:') self.ctrl_combobox = swidgets.SComboBox(items=self.PLOT_TYPES, command=self.setPlotType) self._ctrl_click_label = QtWidgets.QLabel( 'Click in plot to set number of clusters') # Lay out the control frame self._ctrl_layout.addWidget(self._ctrl_label) self._ctrl_layout.addWidget(self.ctrl_combobox) self._ctrl_layout.addStretch() self._ctrl_layout.addWidget(self._ctrl_click_label) self._ctrl_layout.addStretch() self.interior_layout.addLayout(self._ctrl_layout)
[docs] def redraw(self): """ Redraw the plot with the current settings """ if not self.visible: return x_list = self.fp_clu.getNumberOfClustersList() # Get the type of plot to draw if self.plot_type == self.PLOT_TYPES[0]: y_list = self.fp_clu.getKelleyPenaltyList() elif self.plot_type == self.PLOT_TYPES[1]: y_list = self.fp_clu.getRSquaredList() elif self.plot_type == self.PLOT_TYPES[2]: y_list = self.fp_clu.getSemiPartialRSquaredList() elif self.plot_type == self.PLOT_TYPES[3]: y_list = self.fp_clu.getMergeDistanceList() elif self.plot_type == self.PLOT_TYPES[4]: y_list = self.fp_clu.getSeparationRatioList() # Plot the data self.sub_plot.clear() self.sub_plot.plot(x_list, y_list, 'r') self.sub_plot.set_title(self.plot_type) self.sub_plot.set_xlabel("Number of clusters") self.sub_plot.set_ylabel(self.plot_type) # Hook up the mouse click event and show the plot self.canvas.mpl_connect('button_release_event', self.click) self.canvas.show() self.canvas.draw() return
[docs] def setPlotType(self, plot_type): """ Called when the plot type option menu is changed """ self.plot_type = str(plot_type) self.redraw() return
[docs] def show(self): """ Show the plot dialog """ self.visible = True self.redraw() appframework.AppFramework.show(self) # These next two lines help ensure that the plot shows the first time # without a corrupt background. self.canvas.flush_events() self.repaint()
[docs] def close(self): """ Dismiss the window """ self.visible = False self.closePanel()
[docs] def click(self, event): """ Click in plot handler """ if not self.canvas.toolbar.mode and event.inaxes and event.button == 1: num_clu = int(round(event.xdata)) self.num_clusters_selected.emit(num_clu)
# TODO: Port to AppFramework2, factor out common code shared with # ClusterStatisticsPlotDialog class.
[docs]class DendrogramPlotDialog(appframework.AppFramework): """ A class which displays a dialog with a plot of the dendrogram from the most recent clustering """ num_clusters_selected = QtCore.pyqtSignal(int)
[docs] def __init__(self, canvas_cluster=None): """ Create an instance of the dialog. Either canvas_cluster should be specified (CanvasFingerprintCluster object with data) :type canvas_cluster: CanvasFingerprintCluster object :param canvas_cluster: object that contains the clustering statistics """ # Store arguments self.fp_clu = canvas_cluster # TODO: Reimplement as a signal: self.visible = False # Create the window buttons = {'close': {'command': self.close}} appframework.AppFramework.__init__(self, buttons=buttons, title='Dendrogram', subwindow=True) # Create the plot self.canvas = smatplotlib.SmatplotlibCanvas(width=5, height=4, layout=self.interior_layout) self.sub_plot = self.canvas.figure.add_subplot(111) self.canvas.mpl_connect('button_release_event', self.click) self.canvas.show() # Set the cursor equal to a horizontal line cursor = Cursor(self.sub_plot, useblit=True, color='red', linewidth=0.8) cursor.vertOn = False # Add a label and pack them up self._ctrl_click_label = QtWidgets.QLabel( 'Click in plot to set number of clusters') self.addCentralWidget(self._ctrl_click_label) self.canvas.mpl_connect('button_release_event', self.click)
[docs] def show(self): """ Show the plot dialog """ self.visible = True self.redraw() appframework.AppFramework.show(self) # These next two lines help ensure that the plot shows the first time # without a corrupt background. self.canvas.flush_events() self.repaint()
[docs] def close(self): """ Dismiss the window """ self.visible = False self.closePanel()
[docs] def redraw(self): """ Redraw the plot """ if not self.visible: return self.sub_plot.clear() (lines, x_axis_ticks, x_axis_tick_labels) = \ self.fp_clu.getDendrogramData() maxX = 0 for line in lines: self.sub_plot.plot(line[0], line[1], 'k', linewidth=0.2) lx = max(line[0]) if lx > maxX: maxX = lx # Set up the misc. plot details self.sub_plot.set_title("Dendrogram") self.sub_plot.set_xlabel("Structure") self.sub_plot.set_ylabel("Merge Distance") self.sub_plot.set_xticks(x_axis_ticks) self.sub_plot.set_xlim([0, maxX + 1]) self.sub_plot.set_xticklabels(x_axis_tick_labels, size=6, rotation='vertical') # Turn X-axis tick labels off: for line in self.sub_plot.get_xticklines(): line.set_visible(False) self.canvas.show() self.canvas.draw()
[docs] def click(self, event): """ Click in plot handler """ if not self.canvas.toolbar.mode and event.inaxes and event.button == 1: # Merge distance corresponding to user's click in the chart: click_distance = event.ydata merge_distances = self.fp_clu.getMergeDistanceList() for i, md in reversed(list(enumerate(merge_distances))): if click_distance < md: num_clu = i + 1 self.num_clusters_selected.emit(num_clu) break
# TODO: Port to AppFramework2
[docs]class DistanceMatrixPlotDialog(appframework.AppFramework): """ A class which displays a dialog with a plot of the distance matrix associated with the most recent clustering """ PLOT_TYPES = ["Cluster Order", "Original Order"]
[docs] def __init__(self, canvas_cluster, num_clusters_edit=None, distance_matrix_callback=None, structures=True, num_clusters=None): """ Create an instance of the dialog. :type canvas_cluster: CanvasFingerprintCluster object :param canvas_cluster: object that contains the clustering statistics :type num_clusters_edit: QLineEdit :param num_clusters_edit: the widget that contains the number of clusters :type distance_matrix_callback: function :param distance_matrix_callback: function called with the IDs of the structures which are clicked :type structures: bool :param structures: True if the distance matrix should show structures when the user clicks on the plot, False if not """ # Store arguments self.fp_clu = canvas_cluster self.num_clusters_edit = num_clusters_edit self.num_clusters = num_clusters self.distance_matrix_callback = distance_matrix_callback self.structures = structures # Initialize some properties self.visible = False self.colorbar = None self.original_mat = None self.cluster_mat = None self.num_col_edit = None # Create the window buttons = {'close': {'command': self.close}} appframework.AppFramework.__init__(self, buttons=buttons, title='Distance Matrix', subwindow=True) # Create the two master layouts self._data_layout = swidgets.SHBoxLayout() self._options_layout = swidgets.SVBoxLayout() self.interior_layout.addLayout(self._data_layout) self.interior_layout.addLayout(self._options_layout) # Now the two data sublayouts if self.structures: self._structures_layout = swidgets.SVBoxLayout() self._data_layout.addLayout(self._structures_layout) # Now the two option sublayouts self._opt1_layout = swidgets.SHBoxLayout() self._opt2_layout = swidgets.SHBoxLayout() self._options_layout.addLayout(self._opt1_layout) self._options_layout.addLayout(self._opt2_layout) # Create the matplotlib plot self.canvas = smatplotlib.SmatplotlibCanvas(width=5, height=5) self.sub_plot = self.canvas.figure.add_subplot(111, aspect='equal') self.canvas.mpl_connect('button_release_event', self.click) self.canvas.show() self.picking_rect = Rectangle((0.0, 0.0), 1.0, 1.0, fill=True, edgecolor='k', facecolor='white') self.interior_layout.insertWidget(0, self.canvas.toolbar) self._data_layout.insertWidget(0, self.canvas) if self.structures: # Create the structure picture for the X-axis structure self.x_structure = structure2d.StructurePicture() self.x_structure.model.setShowHydrogenPreference(0) self.x_structure.model.setBondLineWidth(2) self.x_structure.model.setAtomRadius(1.2) self.x_structure.model.setTransparent(False) # And now the Y-axis structure self.y_structure = structure2d.StructurePicture() self.y_structure.model.setShowHydrogenPreference(0) self.y_structure.model.setBondLineWidth(2) self.y_structure.model.setAtomRadius(1.2) self.y_structure.model.setTransparent(False) # Now fill the structure region self.x_struct_label = QtWidgets.QLabel('X-Axis structure:') self.x_title_label = QtWidgets.QLabel('') self.y_struct_label = QtWidgets.QLabel('Y-Axis structure:') self.y_title_label = QtWidgets.QLabel('') self._structures_layout.addWidget(self.x_struct_label) self._structures_layout.addWidget(self.x_structure) self._structures_layout.addWidget(self.x_title_label) self._structures_layout.addWidget(self.y_struct_label) self._structures_layout.addWidget(self.y_structure) self._structures_layout.addWidget(self.y_title_label) # Now the first row of options self.option_label = QtWidgets.QLabel('Show distance matrix in:') self.plot_option = swidgets.SComboBox(items=self.PLOT_TYPES, command=self.setPlotType) if self.structures: self.plot_label = QtWidgets.QLabel( 'Click in plot to display structures.') self._opt1_layout.addWidget(self.plot_label) self.plot_include_toggle = QtWidgets.QCheckBox( 'Include clicked structures in Workspace') self._opt1_layout.addWidget(self.option_label) self._opt1_layout.addWidget(self.plot_option) self._opt1_layout.addSpacing(8) self._opt1_layout.addSpacing(8) self._opt1_layout.addWidget(self.plot_include_toggle) self._opt1_layout.addStretch() # And the second row of options bad_colormaps = set( ['brg', 'bwr', 'gist_rainbow', 'seismic', 'terrain']) colormaps = [] for m in list(cm.datad): if not m.endswith("_r") and m not in bad_colormaps: colormaps.append(m) def mapkey(value): return value.lower() colormaps.sort(key=mapkey) self.cmap_label = QtWidgets.QLabel('Colormap:') self.cmap_combobox = swidgets.SComboBox(items=colormaps, command=self.setColormap, default_item='jet') self.num_col_label = QtWidgets.QLabel('Number of colors:') self.num_col_edit = QtWidgets.QLineEdit('100') # Redraw the plot when the user types in a new number of colors self.num_col_edit.editingFinished.connect(self.draw) # FIXME use the new API for connecting the signal self.num_col_edit.setValidator(swidgets.SNonNegativeIntValidator()) self.num_col_edit.setMaximumWidth(60) self._opt2_layout.addWidget(self.cmap_label) self._opt2_layout.addWidget(self.cmap_combobox) self._opt2_layout.addWidget(self.num_col_label) self._opt2_layout.addWidget(self.num_col_edit) self._opt2_layout.addStretch()
[docs] def setPlotType(self, plot_type): """ Called when the plot type combobox is changed :type plot_type: string :param plot_type: the new plot type """ self.plot_type = str(plot_type) self.draw()
[docs] def setColormap(self, color_map): """ Called when the color map combobox is changed :type color_map: string :param color_map: the new color map """ self.color_map = str(color_map) self.draw()
[docs] def redraw(self): """ Force a redraw with refetching of the distance map data """ if self.structures: self.x_structure.clear() self.y_structure.clear() self.original_mat = None self.cluster_mat = None self.draw()
[docs] def draw(self): """ Called when the plot type option menu is changed """ if self.num_col_edit is None: # Leave if redraw is called before plot object constucted return if self.original_mat is None or self.cluster_mat is None: # Need to read the distance matrix file: self.n = len(self.fp_clu.getNumberOfClustersList()) self.original_mat = numpy.zeros((self.n, self.n), numpy.double) self.cluster_mat = numpy.zeros((self.n, self.n), numpy.double) if self.num_clusters_edit: try: num_clusters = int(self.num_clusters_edit.text()) except ValueError: # Can happen for empty edits maestro.warning('Please enter a valid number of clusters') return elif self.num_clusters: num_clusters = self.num_clusters else: raise RuntimeError( 'Either num_clusters or num_clusters_edit must be specified' ) cluster_order = self.fp_clu.getClusterOrderMap(num_clusters) self.reverse_cluster_order = {} for eid in list(cluster_order): self.reverse_cluster_order[cluster_order[eid]] = eid self.entry_id_list = [] csv_filename = self.fp_clu.getDistanceMatrixFile() with csv_unicode.reader_open(csv_filename) as fh: csv_reader = csv.reader(fh) for r, row in enumerate(csv_reader): if r == 0: for c, eid in enumerate(row): if c == 0: continue else: self.entry_id_list.append(eid) continue for c, dist in enumerate(row): if c == 0: continue self.original_mat[r - 1, c - 1] = float(dist) r_eid = self.entry_id_list[r - 1] c_eid = self.entry_id_list[c - 1] r_clu_order = int(cluster_order[r_eid]) c_clu_order = int(cluster_order[c_eid]) self.cluster_mat[r_clu_order, c_clu_order] = float(dist) self.sub_plot.clear() try: num_col = int(self.num_col_edit.text()) except ValueError: # Can happen for empty edits maestro.warning('Please enter a valid number of colors') return cmap = cm.get_cmap(self.color_map, num_col) if self.plot_type == self.PLOT_TYPES[0]: matrix = self.cluster_mat else: matrix = self.original_mat pcol = self.sub_plot.pcolormesh(matrix, cmap=cmap, shading='flat') self.sub_plot.set_title(self.plot_type) self.sub_plot.set_xlim(0.0, self.n) self.sub_plot.set_ylim(0.0, self.n) self.sub_plot.set_xlabel("") self.sub_plot.set_ylabel("") if not self.colorbar: self.colorbar = self.canvas.figure.colorbar(pcol, shrink=0.8, extend='neither') else: self.colorbar.set_cmap(cmap) self.colorbar.changed() self.colorbar.draw_all() self.canvas.show() self.canvas.draw() return
[docs] def show(self): """ Show the plot dialog """ self.visible = True self.redraw() appframework.AppFramework.show(self) # These next two lines help ensure that the plot shows the first time # without a corrupt background. self.canvas.flush_events() self.repaint()
[docs] def close(self): """ Dismiss the window """ if self.structures: self.x_structure.clear() self.y_structure.clear() self.visible = False self.closePanel()
[docs] def click(self, event): """ Click in plot handler """ if not self.canvas.toolbar.mode and event.inaxes and event.button == 1: pt = maestro.project_table_get() x = int(event.xdata) y = int(event.ydata) if self.plot_type == self.PLOT_TYPES[0]: x_eid = self.reverse_cluster_order[x] y_eid = self.reverse_cluster_order[y] else: x_eid = self.entry_id_list[x] y_eid = self.entry_id_list[y] if self.structures: self.drawStructure(self.x_structure, x_eid, self.x_title_label) self.drawStructure(self.y_structure, y_eid, self.y_title_label) # If the "Include in Workspace" toggle is on then we will also # issue Maestro commands to include the associated structures if self.plot_include_toggle.isChecked(): maestro.command('entrywsincludeonly entry "%d"' % int(x_eid)) maestro.command('entrywsinclude entry "%d"' % int(y_eid)) # Make the WS resize to fit these entries maestro.command('fit entry.id %s, %s' % (x_eid, y_eid)) self.picking_rect.set_x(x) self.picking_rect.set_y(y) self.picking_rect.set_visible(True) self.sub_plot.add_patch(self.picking_rect) self.sub_plot.draw_artist(self.picking_rect) self.canvas.show() self.canvas.draw() if self.distance_matrix_callback: self.distance_matrix_callback(x_eid, y_eid)
[docs] def drawStructure(self, canv, eid, title_label): """ Draw the structure from the project with entry id 'eid' in the Canvas 'canv' """ pt = maestro.project_table_get() st = pt[eid].getStructure() title = pt[eid]['s_m_title'] if len(title) > 50: title = title[0:50] + "..." title_label.setText(title) # Use Canvas renderer to create a QPicture canv.drawStructure( st, canvas2d.ChmMmctAdaptor.StereoFromAnnotationAndGeometry_Safe)
def _add_cluster_statistics(id, fp_clu, st, row): """ A private function used to add the clustering statistics to a Structure object or Project table row. Only one of 'st' or 'row' should be not None """ props = fp_clu.getClusteringStatistics(id) if st is not None: st.property.update(props) else: for prop, value in props.items(): row[prop] = value