Source code for schrodinger.application.jaguar.gui.tabs.scan_tab

import enum
import math
from collections import OrderedDict
from past.utils import old_div

import schrodinger
from schrodinger.infra import mm
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt

from .. import ui
from .. import utils as gui_utils
from . import coordinates

maestro = schrodinger.get_maestro()
try:
    from schrodinger.maestro import markers
except ImportError:
    markers = None

COORDINATE_TYPES = OrderedDict(
    (("Dihedral", mm.MMJAG_COORD_TORSION), ("Angle", mm.MMJAG_COORD_ANGLE),
     ("Distance", mm.MMJAG_COORD_DISTANCE), ("Cartesian - X",
                                             mm.MMJAG_COORD_CART_X),
     ("Cartesian - Y", mm.MMJAG_COORD_CART_Y), ("Cartesian - Z",
                                                mm.MMJAG_COORD_CART_Z)))


[docs]class ScanCoordinateColumns(): """ Constants for the full (i.e. hidden as well) table columns """ NAMES = ('Atom Indices', 'Coordinate', 'Type', 'Steps', 'Current Value', 'Starting Value', 'Final Value', 'Increment') NUM_COLS = len(NAMES) (INDICES, COORD_NAME, COORD_TYPE, STEPS, CURRENT_VAL, START_VAL, FINAL_VAL, INCREMENT) = list(range(NUM_COLS))
[docs]class ScanTab(coordinates.CoordinateTab): NAME = "Scan" HELP_TOPIC = "JAGUAR_TOPIC_SCAN_FOLDER" UI_MODULES = (ui.scan_tab_ui,) COLUMN = ScanCoordinateColumns() MAX_ROW_COUNT = 5
[docs] def setup(self): super(ScanTab, self).setup() self.picker = coordinates.CoordinatePicker(COORDINATE_TYPES, self.ui.pick_cb, self.ui.coord_type_combo, self.ui.pick_combo) # create validators double_validator = QtGui.QDoubleValidator() double_validator.setDecimals(3) for widget in [ self.ui.starting_le, self.ui.final_le, self.ui.increment_le ]: widget.setValidator(double_validator) # setup coordinate table self.model = ScanCoordinatesModel(self) self.proxy = ScanCoordinatesProxyModel(self) self.proxy.setSourceModel(self.model) self.ui.tableView.setModel(self.proxy) self.mapper = QtWidgets.QDataWidgetMapper() self.mapper.setModel(self.model) self.mapper.setItemDelegate(ScanCoordinatesDelegate(self)) self.mapper.addMapping(self.ui.current_le, self.COLUMN.CURRENT_VAL) self.mapper.addMapping(self.ui.starting_le, self.COLUMN.START_VAL) self.mapper.addMapping(self.ui.final_le, self.COLUMN.FINAL_VAL) self.mapper.addMapping(self.ui.increment_le, self.COLUMN.INCREMENT) # create connections self.ui.delete_btn.clicked.connect(self.deleteCurrentRow) self.ui.tableView.selectionModel().selectionChanged.connect( self.updateMapperWidgets) self.ui.tableView.selectionModel().selectionChanged.connect( self._highlightSelectedMarkers) self.model.dataChanged.connect(self.updateTotalStructures) self.model.dataChanged.connect(self.proxy.dataChanged) self.picker.pickCompleted.connect(self.pickCompleted)
def _resetDefaults(self): """ This function resets panel to default state. Note that this function is not called reset() since it does not need to be called from the panel class. """ super(ScanTab, self)._resetDefaults() for widget in [ self.ui.starting_le, self.ui.final_le, self.ui.increment_le ]: widget.setText("") widget.setEnabled(False) self.ui.current_le.setText("") self.ui.num_struct_le.setText("1")
[docs] def getMmJagKeywords(self): """ This function returns dictionary of mmjag keywords for this tab. Since this tab does not set any keywords it returns an empty dictionary. :return: mmjag keywords dictionary :rtype: dict """ keywords = {} return keywords
[docs] def loadSettings(self, jag_input): """ Restore scan coordinates settings from Jaguar handle. :param jag_input: The Jaguar settings to base the tab settings on :type jag_input: `schrodinger.application.jaguar.input.JaguarInput` """ self._resetDefaults() # check that there is just single entry in workspace try: st = maestro.get_included_entry() except RuntimeError as err: # Reset panel if there is no entry or there is more than one. # There may be a better way to do this. return num_coords = jag_input.scanCount() for i in range(1, num_coords + 1): (coord_type, atoms, initial, final, num_steps, step) = \ jag_input.getScanCoordinate(i) self.addCoordinate(st, atoms, coord_type, initial, final, step) if num_coords: self.refreshMarkers.emit()
[docs] def saveSettings(self, jag_input, eid=None): """ Save scan coordinate settings in jaguar handle. See parent class for argumnet documentation """ for coord in self.model.coords: atoms = coord.atom_indices jag_input.setScanCoordinate(coord.coordinate_type, atoms, coord.start_value, coord.final_value, coord.num_steps, coord.increment)
[docs] def updateMapperWidgets(self, selected, deselected): """ This slot is called when selection in coordinates table is changed. :param selected: selected indices :type selected: `QtCore.QItemSelection` :param deselected: deselected indices :type deselected: `QtCore.QItemSelection` """ if len(selected) == 0: self.mapper.setCurrentIndex(-1) self.enableSelectedCoordinates(False) else: self.enableSelectedCoordinates(True) index = selected.indexes()[0] self.mapper.setCurrentIndex(index.row())
[docs] def enableSelectedCoordinates(self, enable): """ This function is called to enable/disable widgets in 'selected coordinate' box. When enable argument is False we also clear text in all widgets. :param enable: True/False to enable/disable widgets :type enable: bool """ widgets = [self.ui.starting_le, self.ui.final_le, self.ui.increment_le] for w in widgets: w.setEnabled(enable) if not enable: widgets.extend([self.ui.current_le]) for w in widgets: w.setText("")
[docs] def deleteCurrentRow(self): """ This function is called to delete row which is currently selected from the coordinates table. """ selected = self.ui.tableView.selectedIndexes() if len(selected) == 0: return selected_row = selected[0].row() atoms = self._getAtomsForRow(selected_row) self._emitCoordinateDeleted(atoms) self.model.removeRow(selected_row) self.ui.tableView.clearSelection() if self.model.rowCount() == 0: self.ui.delete_btn.setDisabled(True) self.updateTotalStructures()
[docs] def deleteAllRows(self): """ This function is called to delete all rows from the coordinates table. """ self.ui.tableView.clearSelection() self.model.reset() self._marker_count.clear() self.allCoordinatesDeleted.emit()
[docs] def addCoordinate(self, st, atoms, coordinate_type, start_value=None, final_value=None, increment=None): """ Add new coordinate row. :param st: structure :type st: `schrodinger.structure.Structure` :param atoms: atom indices :type atoms: list :param coordinate_type: coordinate type :type coordinate_type: int :param start_value: starting coordinate value :type start_value: float :param final_value: final coordinate value :type final_value: float :param increment: increment value :type increment: float """ err = self._determineIfConstraintsAddable() if err is not None: self.error(err) elif self.model.rowCount() == self.MAX_ROW_COUNT: self.warning("Can only add maximum of 5 coordinates") elif self.model.addCoordinate(st, atoms, coordinate_type, start_value, final_value, increment): self._emitCoordinateAdded(atoms, coordinate_type) # select row that was just added last_row = self.model.rowCount() - 1 self.ui.tableView.selectRow(last_row) self.ui.delete_btn.setEnabled(True) self.updateTotalStructures()
[docs] def updateTotalStructures(self): """ Calculate total number of structures to be calculated and update the label. """ total = 1 for coord in self.model.coords: total = total * coord.num_steps self.ui.num_struct_le.setText(str(total))
[docs] def pickCompleted(self, atoms): """ This slot is called when required number of atoms for the current coordinate type has been picked. :param atoms: list of atom indices :type atoms: list """ try: st = maestro.get_included_entry() except RuntimeError as err: self.warning(str(err)) return coord_type = self.ui.coord_type_combo.currentData() self.addCoordinate(st, atoms, coord_type)
[docs]class ScanTabNextGeom(ScanTab): """ A scan tab that allows the user to configure how the determine the next initial geometry """ UI_MODULES = (ui.scan_tab_ui, ui.scan_tab_nextgeom_ui) NextGeomFrom = enum.Enum("NextGeomFrom", ["Init", "Prev"])
[docs] def setup(self): # See BaseTab class for method documentation super(ScanTabNextGeom, self).setup() self.reset()
[docs] def reset(self): # See BaseTab class for method documentation self.ui.nextgeom_init_rb.setChecked(True)
[docs] def nextGeom(self): """ Return the setting for the next initial geometry :return: The next initial geometry settings :rtype: `NextGeomFrom` """ if self.ui.nextgeom_init_rb.isChecked(): return self.NextGeomFrom.Init else: return self.NextGeomFrom.Prev
[docs]class ScanCoordinateData(coordinates.CoordinateData): """ This class stores all data for a single scan coordinate. :cvar COORDINATE_FUNCS: dictionary that maps coordinate type to mmct function uses to calculate coordinate value. :vartype COORDINATE_FUNCS: dict :ivar st: ct structure for which coordinates are defined :vartype st: `schrodinger.structure.Structure` :ivar atom_indices: indices of atoms, which define this coordinate :vartype atom_indices: list :ivar coordinate_name: name of this coordinate based on atom indices :vartype coordinate_name: str :ivar coordinate_type: coordinate type :vartype coordinate_type: int :ivar num_steps: number of steps :vartype num_steps: int :ivar current_value: current value of this coordinate :vartype current_value: float :ivar start_value: starting coordinate value :vartype start_value: float :ivar final_value: final coordinate value :vartype final_value: float :ivar increment: increment value :vartype increment: float """ COORDINATE_FUNCS = { mm.MMJAG_COORD_CART_X: mm.mmct_atom_get_x, mm.MMJAG_COORD_CART_Y: mm.mmct_atom_get_y, mm.MMJAG_COORD_CART_Z: mm.mmct_atom_get_z, mm.MMJAG_COORD_DISTANCE: mm.mmct_atom_get_distance, mm.MMJAG_COORD_ANGLE: mm.mmct_atom_get_bond_angle, mm.MMJAG_COORD_TORSION: mm.mmct_atom_get_dihedral_angle } COORDINATE_DISTANCE_OFFSET = 0.2 COORDINATE_DISTANCE_INCREMENT = 0.1 COORDINATE_ANGLE_OFFSET = 20.0 COORDINATE_ANGLE_INCREMENT = 5.0
[docs] def __init__(self, st, atoms, coordinate_type, start_value=None, final_value=None, increment=None): """ Initialize coordinates data given a structure, set of atom indices and coordinate type. :param st: structure :type st: `schrodinger.structure.Structure` :param atoms: atom indices :type atoms: list :param coordinate_type: coordinate type :type coordinate_type: int :param start_value: starting coordinate value :type start_value: float :param final_value: final coordinate value :type final_value: float :param increment: increment value :type increment: float """ super(ScanCoordinateData, self).__init__(st, atoms, coordinate_type) self.current_value = None self.start_value = start_value self.final_value = final_value self.increment = increment self.coordinate_name = self._getCoordinateName() self.current_value = self._getCurrentValue() if self.start_value is None: self._setDefaultValues() else: self._setNumberOfSteps()
def _getCurrentValue(self): """ This function return the current value of coordinate. :return: coordinate current value :rtype: float """ coord_func = self.COORDINATE_FUNCS[self.coordinate_type] args = [] for atom in self.atom_indices: args.append(self.st) args.append(atom) return coord_func(*args) def _setDefaultValues(self): """ This function sets default start, final and increment values for this coordinate. """ if self.current_value is None: self.current_value = self._getCurrentValue() if self.coordinate_type == mm.MMJAG_COORD_ANGLE or \ self.coordinate_type == mm.MMJAG_COORD_TORSION: self.start_value = self.current_value - self.COORDINATE_ANGLE_OFFSET self.final_value = self.current_value + self.COORDINATE_ANGLE_OFFSET self.increment = self.COORDINATE_ANGLE_INCREMENT else: self.start_value = self.current_value - self.COORDINATE_DISTANCE_OFFSET self.final_value = self.current_value + self.COORDINATE_DISTANCE_OFFSET self.increment = self.COORDINATE_DISTANCE_INCREMENT self._setNumberOfSteps() def _setNumberOfSteps(self): """ This function sets number of steps between start and final values. Use the same equation that mmjag's scan.c uses. """ epsilon = 1.e-10 increment = math.fabs(self.increment) delta = math.fabs(self.final_value - self.start_value) if increment < epsilon: self.num_steps = 1 else: # adding epsilon here is needed to deal with roundoff errors self.num_steps = 1 + int(old_div((delta + epsilon), increment))
[docs]class ScanCoordinatesDelegate(QtWidgets.QItemDelegate): """ This delegate is used to define how float coordinate values are displayed in a line edit widget. This class is needed for mapping between table view and other widgets as defined via QDataWidgetMapper. """ COLUMN = ScanCoordinateColumns()
[docs] def setEditorData(self, editor, index): """ This function is used to initialize editor with the relevant data. :param editor: editor :type editor: `QtWidgets.QWidget` :param index: index of data in source model :type index: `QtCore.QModelIndex` """ if editor.metaObject().className() == "QLineEdit": col = index.column() if col == self.COLUMN.STEPS: value = int(index.data()) s = "%d" % value else: value = float(index.data()) s = "%.3f" % value editor.setProperty("text", s) return super(ScanCoordinatesDelegate, self).setEditorData(editor, index)
[docs] def setModelData(self, editor, model, index): """ This function is responsible for transferring data from the editors back to the model. So, here we convert text string into float number. :param editor: editor :type editor: `QtWidgets.QWidget` :param model: data model :type model: `QtCore.QAbstractItemModel` :param index: index of data in source model :type index: `QtCore.QModelIndex` """ if editor.metaObject().className() == "QLineEdit": try: value = float(editor.property("text")) model.setData(index, value) except ValueError: super(ScanCoordinatesDelegate, self).setModelData(editor, model, index)
[docs]class ScanCoordinatesProxyModel(QtCore.QSortFilterProxyModel): """ A proxy model that allows to hide columns. """ COLUMN = ScanCoordinateColumns()
[docs] def __init__(self, parent): super(ScanCoordinatesProxyModel, self).__init__(parent) self.visible_columns = (self.COLUMN.COORD_NAME, self.COLUMN.COORD_TYPE, self.COLUMN.STEPS) # maintain Qt4 dynamicSortFilter default self.setDynamicSortFilter(False)
[docs] def filterAcceptsColumn(self, column, index): """ Modified from the parent class to define columns that should be visible. :param column: the column index :type column: int :param index: Unused, but kept for PyQt compatibility :type index: `QModelIndex` """ return column in self.visible_columns
[docs]class ScanCoordinatesModel(coordinates.CoordinatesModel): """ A model to store scan tab coordinates data. """ COLUMN = ScanCoordinateColumns()
[docs] def addCoordinate(self, st, atoms, coordinate_type, start_value=None, final_value=None, increment=None): """ Add new coordinate row. :param st: structure :type st: `schrodinger.structure.Structure` :param atoms: atom indices :type atoms: list :param coordinate_type: coordinate type :type coordinate_type: int :param start_value: starting coordinate value :type start_value: float :param final_value: final coordinate value :type final_value: float :param increment: increment value :type increment: float :return: returns True if this is a new coordinate and False otherwise. :rtype: bool """ if self.checkNewCoordinate(atoms, coordinate_type): new_row_num = len(self.coords) self.beginInsertRows(QtCore.QModelIndex(), new_row_num, new_row_num) new_coord = ScanCoordinateData(st, atoms, coordinate_type, start_value, final_value, increment) self.coords.append(new_coord) self.endInsertRows() return True return False
[docs] def data(self, index, role=Qt.DisplayRole): """ Retrieve the requested data :param index: The index to retrieve data for :type index: `PyQt5.QtCore.QModelIndex` :param role: The role to retrieve data for :type role: int :return: The requested data """ if role == Qt.TextAlignmentRole: return Qt.AlignLeft elif role == Qt.DisplayRole or role == Qt.EditRole: row = index.row() col = index.column() coord = self.coords[row] if col == self.COLUMN.INDICES: return coord.atom_indices elif col == self.COLUMN.COORD_NAME: return coord.coordinate_name elif col == self.COLUMN.COORD_TYPE: type_text = gui_utils.find_key_for_value( COORDINATE_TYPES, coord.coordinate_type) return type_text elif col == self.COLUMN.STEPS: return coord.num_steps elif col == self.COLUMN.CURRENT_VAL: return coord.current_value elif col == self.COLUMN.START_VAL: return coord.start_value elif col == self.COLUMN.FINAL_VAL: return coord.final_value elif col == self.COLUMN.INCREMENT: return coord.increment
[docs] def setData(self, index, value, role=Qt.EditRole): """ Modify coordinate values. :param index: the index of table cell :type index: `QtCore.QModelIndex` :param value: new value :param role: The role to set data for. :type role: int """ if index.isValid() and role == Qt.EditRole: row = index.row() col = index.column() coord = self.coords[row] value = float(value) if col == self.COLUMN.START_VAL: coord.start_value = value elif col == self.COLUMN.FINAL_VAL: coord.final_value = value elif col == self.COLUMN.INCREMENT: coord.increment = value coord._setNumberOfSteps() left_index = self.index(row, 0) right_index = self.index(row, self.COLUMN.NUM_COLS) self.dataChanged.emit(left_index, right_index) return True else: return super(ScanCoordinatesModel, self).setData(index, value, role)