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