Source code for schrodinger.ui.qt.atom_weights_table_widget

from typing import List
from typing import Optional

import schrodinger
from schrodinger import comparison
from schrodinger import structure
from schrodinger.graphics3d import sphere
from schrodinger.models import mappers
from schrodinger.models import parameters
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.ui.qt import atom_weights_table_ui
from schrodinger.ui.qt import basewidgets
from schrodinger.ui.qt.mapperwidgets import MappableComboBox
from schrodinger.ui.qt.mapperwidgets import plptable
from schrodinger.ui.qt.mapperwidgets.plptable import FieldColumn
from schrodinger.ui.qt.standard import colors

maestro = schrodinger.get_maestro()

INITIAL_WEIGHT = 0.5

COMBO_WIDTH = 100

INFO_TOOLTIP = ('Assign weights from 0 to 1 to indicate'
                '\ntheir relative importance')

SORT_ROLE = QtCore.Qt.UserRole + 1


[docs]class AtomWeight(parameters.CompoundParam): """ Represent weight parameters for a single atom. """ atom_number: int element: str pdb_name: str weight: float
[docs]class AtomWeightsSpec(plptable.TableSpec): """ Table spec for the custom weights. """ atom_number = FieldColumn(AtomWeight.atom_number, title='Atom #') element = FieldColumn(AtomWeight.element, title='Element') pdb_name = FieldColumn(AtomWeight.pdb_name, title='PDB Name') weight = FieldColumn(AtomWeight.weight, title='Weight')
[docs] @atom_number.data_method(SORT_ROLE) @element.data_method(SORT_ROLE) @pdb_name.data_method(SORT_ROLE) @weight.data_method(SORT_ROLE) def sort_role(self, field): return field
[docs] @weight.data_method(role=QtCore.Qt.FontRole) def weight_font(self, weight): font = QtGui.QFont() if weight == INITIAL_WEIGHT: font.setItalic(True) return font
[docs] @weight.data_method(role=QtCore.Qt.ForegroundRole) def weight_color(self, weight): if weight == INITIAL_WEIGHT: color = QtGui.QColor(colors.NativeColors.GRAY) else: color = QtGui.QColor(colors.NativeColors.BLACK) brush = QtGui.QBrush(color) return brush
[docs]class AtomWeightsTableWidget(plptable.PLPTableWidget): """ Table to display atom weights data. """
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setSpec(AtomWeightsSpec()) sort_proxy_model = QtCore.QSortFilterProxyModel() sort_proxy_model.setSortRole(SORT_ROLE) self.addProxy(sort_proxy_model) self._setUpTableView()
def _setUpTableView(self): self.view.setSortingEnabled(True) self.view.setAlternatingRowColors(True) self.view.horizontalHeader().setStretchLastSection(True) self.view.verticalHeader().hide()
[docs]class AtomWeightsSettings(parameters.CompoundParam): """ Class for storing Custom Weight Options. Contains all logic for updating the table data and spheres. """ allow_custom_weights: bool = False use_custom_weights: bool = False default_weight: float = INITIAL_WEIGHT custom_weight: float = INITIAL_WEIGHT atom_weights: List[AtomWeight] selected_weights: List[AtomWeight] _template_sts: List[structure.Structure] _weight_property: str = None template_stsChanged = QtCore.pyqtSignal(object)
[docs] def initConcrete(self): self._prev_default_weight = self.default_weight self._template_stsChanged.connect(self.template_stsChanged)
@property def template_sts(self) -> List[structure.Structure]: return self._template_sts @template_sts.setter def template_sts(self, template_sts: List[structure.Structure]): """ Set template structures as `template_sts`. :param template_sts: Structures to apply custom atom weights to. """ self._template_sts = template_sts self.updateAtomWeights() @property def template_st(self) -> Optional[structure.Structure]: """ Return the first template structure or None. """ if not self.template_sts: return return self.template_sts[0]
[docs] def updateAtomWeights(self): """ Clear the table model and then add a row for each atom in the current template_st. """ self._prev_default_weight = self.default_weight self.atom_weights.clear() self.selected_weights.clear() if not self.template_st or not self.use_custom_weights: return for atom in self.template_st.atom: weight = self.default_weight if self._weight_property: weight = atom.property.get(self._weight_property, self.default_weight) self.atom_weights.append( AtomWeight(atom_number=atom.index, element=atom.element, pdb_name=atom.pdbname, weight=weight))
[docs] def applyDefaultWeight(self): """ Apply the default weights to all rows that haven't been assigned a custom weight, and update all spheres. """ for atom in self.atom_weights: if atom.weight == self._prev_default_weight: atom.weight = self.default_weight self._prev_default_weight = self.default_weight
[docs] def applyCustomWeight(self): """ Apply the custom weight to currently selected rows and update all spheres. """ for atom in self.selected_weights: atom.weight = self.custom_weight
[docs] def addWeightPropsToTemplateStructures(self, weight_property: str): """ Add `weight_property` as a property to the atoms in all template structures. :param weight_property: the name of the associated weight property """ if not self.use_custom_weights: return if self.atom_weights: self._weight_property = weight_property for weight in self.atom_weights: try: atom = self.template_st.atom[weight.atom_number] except IndexError: msg = ('ERROR: WEIGHTS: Template structure has' 'no atom %i in it.' % weight.atom_number) print(msg) continue atom.property[weight_property] = weight.weight else: for atom in self.template_st.atom: atom.property[weight_property] = self.default_weight
[docs]class WeightSphereManager(mappers.MapperMixin): """ Manages 3D spheres representing atom weights of each individual atoms in AtomWeightsSettings. """ model_class = AtomWeightsSettings OPACITY_DEFAULT = 0.4 OPACITY_SELECTED = 1.0 COLOR_DEFAULT = (0.5, 0.5, 1.0) COLOR_SELECTED = (1.0, 1.0, 0.0) RESOLUTION_DEFAULT = 50
[docs] def __init__(self): super().__init__() self._sphere_group = sphere.SphereGroup() self._setupMapperMixin()
[docs] def defineMappings(self): M = self.model_class if not maestro: return [] return [ (self.updateSpheres, M.atom_weights), (self.updateSpheres, M.selected_weights), (self.updateSpheres, M.use_custom_weights), (self.updateSpheres, M.allow_custom_weights), ]
[docs] def getSignalsAndSlots(self, model): return [ (model.template_stsChanged, self.updateSpheres), ]
[docs] def updateSpheres(self): """ Clear all spheres and, if necessary, create a sphere for each row in the atom weights table with appropriate colors. """ if not maestro: return self.clearSpheres() template_in_ws = False if self.model.template_st: template_in_ws = comparison.are_same_geometry( maestro.workspace_get(), self.model.template_st) if all((self.model.allow_custom_weights, self.model.use_custom_weights, template_in_ws)): self._createSpheres()
[docs] def clearSpheres(self): """ Delete all spheres from the sphere group. Clears all spheres in the WS. """ self._sphere_group.clear()
def _createSpheres(self): """ Create a new sphere for each atom in the table with appropriate size and colors. """ template_st = self.model.template_st for atom_weight in self.model.atom_weights: atom = template_st.atom[atom_weight.atom_number] radius = 0.5 * atom_weight.weight selected = False if atom_weight in self.model.selected_weights: selected = True self._createSphere(x=atom.x, y=atom.y, z=atom.z, radius=radius, selected=selected) def _createSphere(self, x: float, y: float, z: float, radius: int, selected: bool): """ Create and add a new sphere to the sphere group. :param x: sphere x coordinate :param y: sphere y coordinate :param z: sphere z coordinate :param radius: sphere radius :param selected: whether or not the sphere is currently selected. """ color = self.COLOR_DEFAULT opacity = self.OPACITY_DEFAULT if selected: color = self.COLOR_SELECTED opacity = self.OPACITY_SELECTED sphere_marker = sphere.MaestroSphere(x=x, y=y, z=z, radius=radius, resolution=self.RESOLUTION_DEFAULT, color=color, opacity=opacity) self._sphere_group.add(sphere_marker)
class _StyledDefaultWeightCombo(MappableComboBox): """ An MappableComboBox which makes the initial weight text italicized and gray. """ def __init__(self, parent): super().__init__(parent) # Same width as other combo defined in atom_weights_table_ui self.setFixedWidth(COMBO_WIDTH) def paintEvent(self, event): """ Re-implement QComboBox.paintEvent to use italic font and gray color for the text of the default atom weight. """ painter = QtWidgets.QStylePainter(self) painter.setPen(self.palette().color(QtGui.QPalette.Text)) opt = QtWidgets.QStyleOptionComboBox() self.initStyleOption(opt) if self.currentText() == str(INITIAL_WEIGHT): font = painter.font() font.setItalic(True) painter.setFont(font) pen = QtGui.QPen(QtGui.QColor(colors.NativeColors.GRAY)) painter.setPen(pen) # draw the combobox frame, focusrect, selected, etc. painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, opt) # draw the icon and text painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, opt)
[docs]class AtomWeightsWidget(mappers.MapperMixin, basewidgets.BaseWidget): """ A widget for the custom weights table. User can specify default and custom weights for the atoms and then use the values to run their custom job with an additional WEIGHT_PROPERTY. Initially, set the template structures using setTemplateStructures() method. To add weights to the template structures, call addWeightPropsToTemplateStructures() with the WEIGHT_PROPERTY. Atom weights can be visualized with sphere markers in the workspace by instantiating this alongside `WeightSphereManager` and mapping both views to the same model. All the params of `AtomWeightsSettings` need to be synced up with the parent widget for an ideal arrangement. """ model_class = AtomWeightsSettings ui_module = atom_weights_table_ui ATOM_WEIGHTS = [0, 0.25, 0.5, 0.75, 1]
[docs] def initSetUp(self): super().initSetUp() self.weights_table = AtomWeightsTableWidget(self) self.default_weight_combo = _StyledDefaultWeightCombo(self) self.ui.apply_default_weight_btn.clicked.connect( self._applyDefaultWeight) self.ui.apply_custom_weight_btn.clicked.connect(self._applyCustomWeight) self.ui.get_ws_selection_btn.clicked.connect(self._getWSSelection) self.ui.info_btn.setToolTip(INFO_TOOLTIP) _atom_weights_items = { str(atom_weight): atom_weight for atom_weight in self.ATOM_WEIGHTS } self.ui.custom_weight_combo.addItems(_atom_weights_items) self.default_weight_combo.addItems(_atom_weights_items)
[docs] def initLayOut(self): super().initLayOut() self.ui.default_weight_layout.insertWidget(1, self.default_weight_combo) self.ui.weights_table_layout.addWidget(self.weights_table)
[docs] def defineMappings(self): M = self.model_class return [ (self.weights_table, M.atom_weights), (self.weights_table.selection_target, M.selected_weights), (self.ui.customize_weights_cb, M.use_custom_weights), (self.ui.custom_weight_combo, M.custom_weight), (self.default_weight_combo, M.default_weight), (self._onAllowCustomWeightsChanged, M.allow_custom_weights), (self._onUseCustomWeightsChanged, M.use_custom_weights), (self._onWeightSelectionChanged, M.selected_weights), ]
@property def template_sts(self) -> List[structure.Structure]: """ Return the list of template structures currently held in the model. """ return self.model.template_sts
[docs] def setTemplateStructures(self, template_sts: List[structure.Structure]): """ Set the model's template structures as `template_sts` or [] if query is an `*.ivol` file, and determine whether the get_ws_selection_btn should be enabled. :param template_sts: Structures to apply custom atom weights to. """ self.model.template_sts = template_sts enabled = False if self.model.template_st and maestro: enabled = comparison.are_same_geometry(maestro.workspace_get(), self.model.template_st) self.ui.get_ws_selection_btn.setEnabled(enabled)
[docs] def addWeightPropsToTemplateStructures(self, weight_property): """ Add weight_property as a property to the atoms in all template structures. :type weight_property: str :param weight_property: the name of the associated weight property """ self.model.addWeightPropsToTemplateStructures(weight_property)
def _getWSSelection(self): """ Clear existing table selection and select rows corresponding to current WS atom selection. """ self.model.selected_weights.clear() atom_idxs = maestro.selected_atoms_get() selected_weights = [ atom_weight for atom_weight in self.model.atom_weights if atom_weight.atom_number in atom_idxs ] self.weights_table.setSelectedParams(selected_weights) def _onAllowCustomWeightsChanged(self): """ Enable/disable the "Customize atom weights" check box. """ enabled = self.model.allow_custom_weights self.ui.customize_weights_cb.setEnabled(enabled) def _onUseCustomWeightsChanged(self): """ Update the table when this option is toggled. """ self.model.updateAtomWeights() enabled = self.model.use_custom_weights self.ui.custom_weights_box.setEnabled(enabled) def _onWeightSelectionChanged(self): """ Determine whether to enable the apply_custom_weight button. """ enable = bool(self.model.selected_weights) self.ui.apply_custom_weight_btn.setEnabled(enable) def _applyDefaultWeight(self): self.model.applyDefaultWeight() # Won't immediately show changed weights without this self.weights_table.repaint() def _applyCustomWeight(self): self.model.applyCustomWeight() # Won't immediately show changed weights without this self.weights_table.repaint()