Source code for schrodinger.application.matsci.chargegui

"""
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 getForceFieldMenuItem(self): """ :return: the text as the force field menu item. :rtype: str """ return self.ff_selector.force_field_menu.currentText()
[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