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