"""
Classes to aid in assigning charges to atoms in a GUI
Copyright Schrodinger, LLC. All rights reserved.
"""
import schrodinger
from schrodinger.application.matsci import clusterstruct
from schrodinger.application.matsci import desmondutils
from schrodinger.application.matsci import jagwidgets
from schrodinger.application.matsci import parserutils
from schrodinger.infra import mm
from schrodinger.infra import mmcheck
from schrodinger.structutils import analyze
from schrodinger.ui.qt import forcefield
from schrodinger.ui.qt import propertyselector
from schrodinger.ui.qt import swidgets
DEFAULT_CHARGE_PROP = clusterstruct.DEFAULT_CHARGE_PROP
MONOMER_CHARGE_PROP = 'r_matsci_Monomer_Charges'
# Typical atom properties that we know aren't charge properties - no sense
# in polluting the charge combobox with them
NON_CHARGE_PROPS = set([
'i_m_mmod_type', 'r_m_x_coord', 'r_m_y_coord', 'r_m_z_coord',
'i_m_residue_number', 'i_m_color', 'i_m_atomic_number',
'i_m_representation', 'i_m_visibility', 'i_m_template_index',
'i_m_secondary_structure', 'i_m_pdb_convert_problem', 'r_m_pdb_occupancy',
'r_m_pdb_tfactor', 'i_ppw_water', 'i_ppw_het', 'i_pdb_seqres_index',
'i_pdb_PDB_serial', 'r_epik_H2O_pKa', 'r_epik_H2O_pKa_uncertainty',
'i_i_constraint', 'i_m_Hcount', 'i_m_minimize_atom_index',
mm.M2IO_DATA_ATOM_ISOTOPE_PROPERTY
])
QUANTUM = 'Atoms'
EXISTING_Q = 'Existing partial charges'
ESP = 'ESP Charges'
CHARGE_METHODS = [EXISTING_Q, ESP]
ESP_ARGPARSE_VALUE = clusterstruct.ESP_ARGPARSE_VALUE
BASIS_FLAG = clusterstruct.BASIS_FLAG
USE_Q_FLAG = clusterstruct.USE_Q_FLAG
[docs]class ChargePropertySelector(propertyselector.PropertySelectorMenu):
"""
A widget that allows the user to pick from a list of properties from a
combobox and shows the user the "User" version of the property name
(Property Name rather than x_prog_Property_Name)
"""
[docs] def __init__(self, label, layout=None, tip=None):
"""
Create a Charge PropertySelector instance
:type label: str
:param label: The label for the combobox
:type layout: QBoxLayout
:param layout: The layout to place the SLabeledComboBox into
:type tip: str
:param tip: The tooltip for the combobox
"""
self.combo = swidgets.SLabeledComboBox(label,
layout=layout,
tip=tip,
default_item=DEFAULT_CHARGE_PROP)
self.combo.setSizeAdjustPolicy(self.combo.AdjustToContents)
propertyselector.PropertySelectorMenu.__init__(self, self.combo)
[docs] def setVisible(self, state):
"""
Set the combobox and label visible or hidden
:type state: bool
:param state: Whether to show (True) or hide (False) the widgets
"""
self.combo.setVisible(state)
self.combo.label.setVisible(state)
[docs] def isVisible(self):
"""
Check if the combobox is visible
:rtype: bool
:return: True if visible, False if not
"""
return self.combo.isVisible()
[docs] def reset(self):
"""
Reset the combobox
"""
self.updateProperties(None)
[docs] def updateProperties(self, structs, monomer_charge=True):
"""
Fill the combobox with all allowed atom properties available in structs
:type structs: list or None
:param structs: If a list, each item is a
`schrodinger.structure.Structure` object. Use None to remove all atom
properties but the default
:type monomer_charge: bool
:param monomer_charge: If True, monomer charges shows up in the charge
combo box.
"""
current_selection = self.getSelected()
proplist = [DEFAULT_CHARGE_PROP]
if structs:
if monomer_charge:
for struct in structs:
if struct.atom_total > 0:
struct.atom[1].property[MONOMER_CHARGE_PROP] = 1
proplist = self.getAllPotentialAtomChargeProperties(structs)
self.setProperties(proplist)
# Reselect the current property
try:
self.select(current_selection)
except:
# Unfortunately, the method only raises a generic Exception
try:
self.select(DEFAULT_CHARGE_PROP)
except:
pass
[docs] def getSelectedProp(self):
"""
Return the x_prog_property_name form of the selected property
:rtype: str or None
:return: The x_prog_property_name value of the selected property or None
if no property is selected
"""
selected = self.getSelected()
if selected:
return selected.dataName()
else:
return selected
[docs] @staticmethod
def getAllPotentialAtomChargeProperties(structs):
"""
Accumulate a list of all numeric atomic properties in all structures,
removing any on the NON_CHARGE_PROPS list
:type structs: list
:param structs: Each item is a `schrodinger.structure.Structure`
object.
:rtype: list
:return: A list of all numeric `(r_, i_)` atom properties found in
structs. The list is sorted alphabetically.
"""
all_atom_props = set()
numerical_starters = ['r', 'i']
for struct in structs:
for atom in struct.atom:
numerical_props = [
x for x in list(atom.property) if x[0] in numerical_starters
]
all_atom_props.update(numerical_props)
all_atom_props = all_atom_props.difference(NON_CHARGE_PROPS)
proplist = list(all_atom_props)
proplist.sort()
return proplist
[docs]class NeighborTreatment(swidgets.SFrame):
"""
An SFrame that contains a set of widgets that allows the user to pick
whether neighboring atoms will be dealt with as atoms, existing charges or
on-the-fly ESP charges
"""
[docs] def __init__(self,
quantum=False,
label=None,
basis_set='3-21G',
layout_type=swidgets.HORIZONTAL,
**kwargs):
"""
Create a NeighborTreatment instance
:type quantum: bool
:param quantum: Whether the option to treat the neighbors as atoms is
to be offered
:type label: str
:param label: The leading label on the line
:type basis_set: str
:param basis_set: The basis set to use by default for quantum and ESP
treatments
All other kwargs are passed to the SFrame __init__ method. In
particular, pass in the layout keyword to place these widgets into a
parent layout.
"""
swidgets.SFrame.__init__(self, layout_type=layout_type, **kwargs)
layout = self.mylayout
if label:
self.leading_label = swidgets.SLabel(label, layout=layout)
else:
self.leading_label = None
items = CHARGE_METHODS[:]
if quantum:
items.insert(0, QUANTUM)
self.type_combo = swidgets.SComboBox(items=items,
layout=layout,
command=self.methodChanged,
nocall=True)
self.options_layout = swidgets.SHBoxLayout(layout=layout)
self.basis_selector = jagwidgets.BasisSetSelector(
'with basis set:', basis_set, layout=self.options_layout)
self.prop_selector = ChargePropertySelector('Atom property:',
layout=self.options_layout)
self.methodChanged()
[docs] def methodChanged(self):
"""
React to changing the treatment method
"""
showprop = self.type_combo.currentText() == EXISTING_Q
self.basis_selector.setVisible(not showprop)
self.prop_selector.setVisible(showprop)
[docs] def reset(self):
"""
Reset all the widgets
"""
self.type_combo.reset()
self.basis_selector.reset()
self.prop_selector.reset()
self.methodChanged()
[docs] def updateProperties(self, structs):
"""
Update the available charge properties based on structs
:type structs: list
:param structs: A list of structures to pull charge properties from
"""
self.prop_selector.updateProperties(structs)
if structs:
# The basis selector can only use a single structure for validating
# basis sets
self.basis_selector.setStructure(structs[0])
else:
self.basis_selector.setStructure(None)
[docs] def validate(self):
"""
Validate that the widgets are in a proper state
:rtype: True or (False, msg)
:return: True if everything is OK or (False, msg) if something is wrong.
msg will describe the issue. The return value is consistent with af2
validation methods
"""
if self.type_combo.currentText() == EXISTING_Q:
if not self.prop_selector.getSelected():
return (False, 'No existing charge property was selected - '
'perhaps there are no existing charges?')
else:
return True
else:
result = self.basis_selector.validate()
if not result:
return (False, 'Spectator atoms: ' + result.message)
return True
[docs] def getMethod(self):
"""
Get the current treatment method
:rtype: str
:return: The selected treatment method
"""
return self.type_combo.currentText()
[docs] def getBasis(self):
"""
Get the currently selected basis set if the treatment method requires
one
:rtype: str or None
:return: A current basis set or None if the current treatment does not
use a basis set
"""
if self.basis_selector.isVisible():
return self.basis_selector.getSelection()
else:
return None
[docs] def getQProp(self):
"""
Get the currently selected charge property if applicable
:rtype: str or None
:return: The current charge property or None if the current treatment
does not use a charge property (or none is available). The property will
be in the x_prog_property_name format.
"""
if self.type_combo.currentText() == ESP:
return ESP_ARGPARSE_VALUE
elif self.prop_selector.isVisible():
return self.prop_selector.getSelectedProp()
else:
return None
[docs] def getFlags(self, flag_names=None):
"""
Get the command line flags generated by this set of widgets
:type flag_names: dict
:param flag_names: Used to modify flag names to custom values. Keys are
default flag names (BASIS_FLAG, USE_Q_FLAG module constants) and values
are the new flag string to use.
:rtype: list
:return: A list of command line flags and values that capture the
current state of these widgets.
"""
if not flag_names:
flag_names = {}
flags = []
basis = self.getBasis()
if basis:
flag = flag_names.get(BASIS_FLAG, BASIS_FLAG)
flags += [flag, basis]
qprop = self.getQProp()
if qprop:
flag = flag_names.get(USE_Q_FLAG, USE_Q_FLAG)
flags += [flag, qprop]
return flags
[docs]class ChargeDialog(swidgets.SDialog):
"""
A dialog that allows the user to choose custom charges for atoms
"""
[docs] def layOut(self):
"""
Lay out the dialog widgets
"""
layout = self.mylayout
self.use_custom_charge_gb = swidgets.SGroupBox(
'Use custom charges',
layout=swidgets.VERTICAL,
parent_layout=layout,
tip='The default when unchecked is to generate force field charges',
checkable=True,
checked=False)
# Charge property
self.property_selector = ChargePropertySelector(
'Charge property:',
layout=self.use_custom_charge_gb.layout,
tip='The property that defines the atomic charges to use')
# Charge atoms
alayout = swidgets.SHBoxLayout(layout=self.use_custom_charge_gb.layout)
self.atom_asl_edit = swidgets.SLabeledEdit(
'Apply to atoms:',
layout=alayout,
tip='ASL specifying the atoms the custom charges will be applied to'
)
self.atom_select_button = swidgets.SPushButton(
'Select Atoms...',
command=self.selectAtoms,
layout=alayout,
tip='Open a panel that aids in generating the atom ASL.')
self.maestro = schrodinger.get_maestro()
if not self.maestro:
self.atom_select_button.hide()
[docs] def setEnabledState(self, enabled):
"""
The the enabled state for custom charge widgets.
:param bool enabled: Whether the custom charge widgets are enabled.
"""
self.property_selector.optionmenu.setEnabled(enabled)
self.atom_asl_edit.setEnabled(enabled)
self.atom_select_button.setEnabled(enabled)
[docs] def reset(self):
"""
Reset the dialog
"""
self.use_custom_charge_gb.reset()
self.atom_asl_edit.reset()
self.property_selector.reset()
[docs] def selectAtoms(self):
"""
Open the select atoms panel to help the user build an ASL for
custom-charged atoms
"""
asl = self.maestro.atom_selection_dialog('Atoms With Custom Charge')
if asl:
self.testASL(asl=asl)
else:
# In case it comes back as None
asl = ""
self.atom_asl_edit.setText(asl)
[docs] def testASL(self, asl=None):
"""
Check to see if an ASL is valid
:type asl: str
:param asl: The ASL to check
:rtype: bool
:return: Whether the ASL is valid and matches atoms or not
"""
if not self.master.structs:
return True
if asl is None:
asl = str(self.atom_asl_edit.text())
if not asl:
if self.use_custom_charge_gb.isChecked():
self.warning('An ASL for atoms with custom charges must be '
'specified.')
return False
return True
try:
atom_list = []
for struct in self.master.structs:
atom_list.extend(analyze.evaluate_asl(struct, asl))
except mmcheck.MmException:
self.warning('Error in parsing charge atom ASL:\n%s' % asl)
return False
else:
if not atom_list:
self.warning('The specified charge atom ASL does not match any '
'atoms')
return False
return True
[docs] def updateProperties(self, structs):
"""
Update the properties in the charge property combobox.
:type structs: list or None
:param structs: If a list, each item is a
`schrodinger.structure.Structure` object. Use None to remove all atom
properties but the default
"""
self.property_selector.updateProperties(structs)
[docs] def accept(self):
"""
Allow the user to close the dialog only if the widgets are in a valid
state
"""
if self.validateASL():
return swidgets.SDialog.accept(self)
[docs] def closeEvent(self, event):
"""
Allow the user to close the dialog via the window manager X button only
if the widgets are in a valid state
:type event: `QtGui.QCloseEvent`
:param event: The event object from the triggered event
"""
if self.validateASL():
return swidgets.SDialog.closeEvent(self, event)
else:
event.ignore()
[docs] def validateASL(self):
"""
Check that the custom atom charge ASL is valid. Post a dialog if that
ASL is not valid or matches no atoms.
:rtype: bool
:return: True if everything is OK, False if validation fails.
"""
if self.use_custom_charge_gb.isChecked():
if not self.testASL():
# Test ASL shows a dialog if it is invalid, so return just False
return False
return True
[docs] def getCustomChargeInfo(self):
"""
Return the ASL for custom charge atoms and the custom charge property
:rtype: (str, str)
:return: The ASL specifying which atoms to charge and the name of the
property that stores the custom charges
"""
try:
property_name = self.property_selector.getSelected().dataName()
except AttributeError:
# This can happen if there are no selected properties (getSelected()
# == None) see MATSCI-5293
property_name = DEFAULT_CHARGE_PROP
if self.use_custom_charge_gb.isChecked():
asl = str(self.atom_asl_edit.text())
else:
asl = ""
return asl, property_name
[docs] def allowAtomSelection(self, state):
"""
Enable/disable the atom selection button
:type state: bool
:param state: The enabled state of the button
"""
self.atom_select_button.setEnabled(state)
[docs]class FFInfo(swidgets.SFrame):
"""
A frame that contains information about the chosen forcefield and a button
to change those options
"""
[docs] def __init__(self, ffdialog, layout, cbox=None):
"""
Create an FFInfo object
:type ffdialog: `schrodinger.ui.qt.swidgets.SDialog`
:param ffdialog: The dialog to open when the options button is pressed
:type layout: `QtWidgets.QBoxLayout`
:param layout: The layout to place this frame into
:type cbox: `QtWidgets.QCheckBox`
:param cbox: A checkbox that will control whether this frame is enabled
or not
"""
swidgets.SFrame.__init__(self,
layout=layout,
layout_type=swidgets.HORIZONTAL)
self.ffdialog = ffdialog
self.cbox = cbox
self.ffdialog.ff_changed.connect(self.forceFieldChanged)
tip = ('The force field to use. The same force field is used for all\n'
'functions, including Monte Carlo, minimization and Desmond\n'
'system preparation.')
self.label = swidgets.SLabel("", layout=self.mylayout, tip=tip)
swidgets.SPushButton('Force Field...',
layout=self.mylayout,
command=self.showFFDialog,
tip='Change the force field')
# This updates the ff label
self.forceFieldChanged()
if self.cbox:
self.cbox.toggled.connect(self.enableWithCBox)
self.enableWithCBox()
self.mylayout.addStretch()
[docs] def forceFieldChanged(self, name=None):
"""
Update the label when the forcefield changes
:type name: str
:param name: The name of the new forcefield
"""
if name is None:
name = self.ffdialog.getForceFieldMenuItem()
self.label.setText(name.replace('_', ' '))
[docs] def showFFDialog(self):
"""
Show the force field options dialog
"""
self.ffdialog.show()
self.ffdialog.raise_()
[docs] def enableWithCBox(self):
"""
Set this frame enabled/disabled when the controlling checkbox is
checked/unchecked
"""
if self.cbox:
self.setEnabled(self.cbox.isChecked())
[docs]class ForceFieldDialog(swidgets.SDialog):
"""
A dialog that contains forcefield options
"""
OK = True
NOT_OK = False
[docs] def layOut(self):
"""
Lay out the dialog widgets
"""
layout = self.mylayout
# Force field information
flayout = swidgets.SHBoxLayout(layout=layout)
self.ff_selector = forcefield.ForceFieldSelector(layout=flayout)
swidgets.SPushButton('Custom Charges...',
layout=flayout,
command=self.showChargeDialog)
flayout.addStretch()
self.charge_dlg = ChargeDialog(
self.master,
title='Custom Atom Charges',
help_topic='MATERIALS_SCIENCE_CUSTOM_ATOM_CHARGES')
self.last_ff_check = self.OK
self.ff_changed = self.ff_selector.force_field_menu.currentTextChanged
[docs] def showChargeDialog(self):
"""
Show the dialog that allows custom charges
"""
self.charge_dlg.show()
self.charge_dlg.raise_()
[docs] def reset(self):
"""
Reset the dialog widgets
"""
self.ff_selector.update()
self.charge_dlg.reset()
[docs] def customOPLSDir(self):
"""
Return the custom OPLS directory if one was requested
:rtype: str or None
:return: The path to the custom directory, or None if no such directory
was requested
"""
return self.ff_selector.getCustomOPLSDIR()
[docs] def getFlags(self):
"""
Get the command line flags based on the GUI settings
:rtype: list
:return: List of command line flags and arguments
"""
ff_txt = self.ff_selector.force_field_menu.currentText()
return [parserutils.FLAG_FORCEFIELD, ff_txt]
[docs] def findInvalidFFStructures(self, structs):
"""
Check a list of structures to see if any are invalid with the given
forcefield
:type structs: list
:param structs: A list of structures to check
:rtype: str or bool
:return: If bool, whether any of the structures are invalid. If str,
then invalid structures were found and the str is an error message. An
error message is only returned if the previous state is was all valid.
This avoids continually showing the failure dialog.
"""
ffield_num = self.ff_selector.getSelectionForceFieldInt()
invalid = desmondutils.find_forcefield_invalid_structures(
structs, ffield_num)
if invalid:
if self.last_ff_check is self.OK:
# Make sure the dialog doesn't get too big
if len(invalid) > 10:
invalid = invalid[:10] + ['...']
ffield_name = self.ff_selector.getSelectionForceField()
error = ('The chosen force field, %s, is not valid for the '
'following structures, therefore all force field '
'operations will be disabled.\n%s' %
(ffield_name, '\n'.join([x.title for x in invalid])))
else:
error = True
self.last_ff_check = self.NOT_OK
return error
self.last_ff_check = self.OK
return False