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