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 coordinates
from .. import utils as gui_utils
from .. import ui

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)