Source code for schrodinger.application.jaguar.gui.tabs.molecule_tab

import warnings

import schrodinger
from schrodinger import project
from schrodinger.application.jaguar import basis as jag_basis
from schrodinger.application.jaguar import input as jaginput
from schrodinger.infra import mm
from schrodinger.ui import picking

from .. import ui
from .. import utils as gui_utils
from ..utils import MOLECULAR_CHARGE_PROP
from ..utils import SPIN_MULT_PROP
from ..utils import JaguarSettingError
from ..utils import JaguarSettingWarning
from .base_tab import BaseTab
from .base_tab import ProvidesBasisMixin

maestro = schrodinger.get_maestro()


[docs]class MoleculeTabBase(BaseTab): """ The base class for all molecule tabs. This class defines functionality for the top half of the tab, which is constant between subclasses. """ NAME = "Molecule" HELP_TOPIC = "JAGUAR_TOPIC_MOLECULE_FOLDER" MULTIPLE_STRUC_WARNING = ("Warning: Charge and spin multiplicity\n" "cannot be changed when more than one\n" "entry is selected.") CHARGE_SPIN_INCONSISTANT_WARNING = ("Warning: Charge and spin multiplicity " "are inconsistent.")
[docs] def setup(self): self._fixSpinAndUpdateWarning() self.ui.molecular_charge_sb.valueChanged.connect( self._fixSpinAndUpdateWarning) self.ui.spin_mult_sb.valueChanged.connect(self._fixSpinAndUpdateWarning) self.ui.create_props_btn.clicked.connect(self.createChargeProperties) self.ui.fix_spin_cb.stateChanged.connect(self._fixSpinAndUpdateWarning) if self.input_selector is not None: self.input_selector.input_changed.connect( self._fixSpinAndUpdateWarning)
[docs] def createChargeProperties(self): """ Create charge and spin properties in the project table in response to the user clicking on the Create Properties button. """ err = self._createChargePropertiesSanityCheck() if err: self.error(err) return in_sel = self.input_selector if in_sel.inputState() == in_sel.SELECTED_ENTRIES: selection = "selected" elif in_sel.inputState() == in_sel.INCLUDED_ENTRIES: selection = "included" self._createChargePropertiesForEsl(selection)
def _createChargePropertiesForEsl(self, esl): """ Create charge and spin properties in the project table for the entries described by the provided ESL :param esl: The entry selection language string describing the entries to create properties for :type esl: str """ prop_calc = "propertycalculate recalculate=true propertyname=%s %s" maestro.command("beginundoblock") maestro.command(prop_calc % ("molcharge", esl)) maestro.command(prop_calc % ("spin", esl)) maestro.command("showpanel table") maestro.command("endundoblock") def _createChargePropertiesSanityCheck(self): """ Make sure that we're in a state where charge and spin properties can be created in the project table. :return: If we can't currently create charge and spin properties, an error message will be returned. Otherwise, None will be returned. :rtype: str or NoneType """ in_sel = self.input_selector if in_sel.inputState() == in_sel.FILE: msg = ("Project table properties may not be created when using " "structures from files.") return msg num_strucs = self._getNumStructures() if num_strucs == 0: msg = ("No structures are selected.") return msg # TODO: more than one structure should be allowed elif num_strucs > 1: msg = ("More than one structure selected.") return msg return None def _fixSpinAndUpdateWarning(self): """ Display any applicable warnings about charge and spin. Additionally, any inconsistency between charge and spin will be fixed if the user has requested it (via the "Keep multiplicity consistent with charge" check box). """ num_strucs = self._getNumStructures() if num_strucs > 1: warning = self.MULTIPLE_STRUC_WARNING # TODO: disable widgets? elif num_strucs > 0 and not self._checkAndFixSpin(): warning = self.CHARGE_SPIN_INCONSISTANT_WARNING else: warning = "" self.ui.charge_warning_lbl.setText(warning) def _checkAndFixSpin(self): """ Figure out if the charge and spin are consistent. Any inconsistency will be fixed if the user has requested it (via the "Keep multiplicity consistent with charge" check box). :return: True if the charge and spin are consistent. False otherwise. :rtype: bool :note: This function should only be called when there is exactly one structure selected. """ charge = self.ui.molecular_charge_sb.value() spin = self.ui.spin_mult_sb.value() fix_spin = self.ui.fix_spin_cb.isChecked() struc = self._getStructure() num_protons = gui_utils.calculate_num_protons(struc) num_electrons = num_protons - charge if (num_electrons + spin) % 2: return True elif fix_spin: new_spin = num_electrons % 2 + 1 self.ui.spin_mult_sb.setValue(new_spin) return True else: return False
[docs] def getMmJagKeywords(self): keywords = self._getSymmetryKeywords() if self._getNumStructures() > 1: # When more than one structure is selected, leave charge and spin # settings at their default values # TODO: We can't assume that settings are at their default values, # so we need to explicitly set the defaults here pass elif self.ui.use_from_pt_rb.isChecked(): keywords.update(self._getChargeFromProjTable()) else: keywords.update(self._getChargeFromGui()) return keywords
def _getSymmetryKeywords(self): """ Get the symmetry-related mmjag keywords """ keywords = {} symmetry = str(self.ui.symmetry_combo.currentText()) if symmetry == "Abelian": keywords[mm.MMJAG_IKEY_IDOABE] = mm.MMJAG_IDOABE_ON else: keywords[mm.MMJAG_IKEY_IDOABE] = mm.MMJAG_IDOABE_OFF if symmetry == "Off": keywords[mm.MMJAG_IKEY_ISYMM] = mm.MMJAG_ISYMM_OFF else: keywords[mm.MMJAG_IKEY_ISYMM] = mm.MMJAG_ISYMM_FULL return keywords def _getChargeFromProjTable(self): """ Get the charge and spin mmjag keywords from the project table :raise JaguarSettingError: If the selected structure does not have the required properties. :note: This function should only be called when there is exactly one structure selected. """ struc = self._getStructure() try: charge = struc.property[MOLECULAR_CHARGE_PROP] spin = struc.property[SPIN_MULT_PROP] except KeyError: err = ("The selected structure does not have charge and spin " "properties") raise JaguarSettingError(err) keywords = {mm.MMJAG_IKEY_MOLCHG: charge, mm.MMJAG_IKEY_MULTIP: spin} return keywords def _getChargeFromGui(self): """ Get the charge and spin mmjag keywords from panel spin boxes """ charge = self.ui.molecular_charge_sb.value() spin = self.ui.spin_mult_sb.value() keywords = {mm.MMJAG_IKEY_MOLCHG: charge, mm.MMJAG_IKEY_MULTIP: spin} return keywords
[docs] def loadSettings(self, keywords): self._loadSymmetry(keywords) self._loadCharge(keywords)
def _loadSymmetry(self, keywords): """ Update the panel based on the symmetry-related mmjag keywords """ # For mm.MMJAG_IKEY_ISYMM and mm.MMJAG_IKEY_IDOABE, we assume that all # non-off values (i.e. all non-zero values) are equivalent if keywords[mm.MMJAG_IKEY_ISYMM] == mm.MMJAG_ISYMM_OFF: index = self.ui.symmetry_combo.findText("Off") elif keywords[mm.MMJAG_IKEY_IDOABE] != mm.MMJAG_IDOABE_OFF: index = self.ui.symmetry_combo.findText("Abelian") else: index = self.ui.symmetry_combo.findText("Use if present") self.ui.symmetry_combo.setCurrentIndex(index) def _loadCharge(self, keywords): """ Update the panel based on the charge and spin mmjag keywords """ charge = keywords[mm.MMJAG_IKEY_MOLCHG] spin = keywords[mm.MMJAG_IKEY_MULTIP] self.ui.use_values_rb.setChecked(True) self.ui.molecular_charge_sb.setValue(charge) self.ui.spin_mult_sb.setValue(spin) def _getNumStructures(self): """ Get the number of structures that this panel is currently representing """ return gui_utils.count_num_strucs(self.input_selector) def _getStructure(self): """ Get the structure that this panel is currently representing """ return next(self.input_selector.structures(False))
[docs]class MoleculeTab(ProvidesBasisMixin, MoleculeTabBase): """ The standard molecule tab """ UI_MODULES = (ui.molecule_top_ui, ui.molecule_std_bottom_ui)
[docs] def setup(self): super(MoleculeTab, self).setup() self.ui.basis_selector.basis_changed.connect(self.basis_changed.emit) if self.input_selector is not None: self.ui.basis_selector.setInputSelector(self.input_selector)
[docs] def getDefaultKeywords(self): """ The mmjag default basis set is "", which would cause a warning, so we set the default basis. :return: A keyword dictionary with the default basis set :rtype: dict """ return {mm.MMJAG_SKEY_BASIS: jag_basis.default_basis()}
[docs] def getBasis(self, ignored=None): return self.ui.basis_selector.getBasis()
[docs] def getMmJagKeywords(self): keywords = super(MoleculeTab, self).getMmJagKeywords() keywords[mm.MMJAG_SKEY_BASIS] = self.ui.basis_selector.getBasis() return keywords
[docs] def validate(self): if not self.ui.basis_selector.isValid(): err = ("The currently selected basis set is invalid for the " "current structure.") return err
[docs] def loadSettings(self, keywords): super(MoleculeTab, self).loadSettings(keywords) basis = keywords[mm.MMJAG_SKEY_BASIS] basis = basis.upper() if not basis: warnings.warn(JaguarSettingWarning("No basis set was specified.")) else: try: self.ui.basis_selector.setBasis(basis) except ValueError: msg = ("The specified basis set (%s=%s) was not recognized." % (mm.MMJAG_SKEY_BASIS, basis)) warnings.warn(JaguarSettingWarning(msg))
[docs]class MoleculeTabPka(MoleculeTabBase): """ The molecule tab used in the PKA panel """ MULTIPLE_STRUC_PICK_WARNING = ("Warning: Pick options cannot be used when\n" "more than one entry is in the Workspace") FILE_PICK_WARNING = ("Warning: Pick options cannot be used when\n" "using structures from files.") UI_MODULES = (ui.molecule_top_ui, ui.molecule_pka_bottom_ui) PKA_ATOM_PROP = "s_m_pKa_atom"
[docs] def setup(self): super(MoleculeTabPka, self).setup() pick_atom_msg = "Pick an atom to be treated as the pKa atom" self.pick_this_atom_picker = picking.PickAtomToggle( self.ui.pick_this_atom_cb, self.pickThisAtom, pick_atom_msg) self.pick_proj_table_atom_picker = picking.PickAtomToggle( self.ui.pick_proj_table_atom_cb, self.pickProjTableAtom, pick_atom_msg) self.ui.use_this_atom_rb.toggled.connect(self._pickAtomRbToggled) self.input_selector.input_changed.connect( self.ui.pick_this_atom_le.clear) self.input_selector.input_changed.connect( self._updatePickWarningAndCheckBoxes) self.reset() self._updatePickWarningAndCheckBoxes()
[docs] def reset(self): self.ui.zwitterion_cb.setChecked(True) self.ui.use_this_atom_rb.setChecked(True) self.ui.pick_this_atom_cb.setChecked(False)
def _updatePickWarningAndCheckBoxes(self): """ If there are multiple structures selected or if the input selector is set to File, display a warning. Also update the enabled status of the picking check boxes. """ in_sel = self.input_selector if in_sel.inputState() == in_sel.FILE: warning = self.FILE_PICK_WARNING elif self._getNumWorkspaceStructures() > 1: warning = self.MULTIPLE_STRUC_PICK_WARNING else: warning = "" self.ui.pick_warning_lbl.setText(warning) pick_this_selected = self.ui.use_this_atom_rb.isChecked() enable_pick_this = not warning and pick_this_selected enable_pick_proj_table = not warning and not pick_this_selected self.ui.pick_this_atom_cb.setEnabled(enable_pick_this) self.ui.pick_this_atom_le.setEnabled(enable_pick_this) self.ui.pick_proj_table_atom_cb.setEnabled(enable_pick_proj_table) if not enable_pick_this: self.ui.pick_this_atom_cb.setChecked(False) if not enable_pick_proj_table: self.ui.pick_proj_table_atom_cb.setChecked(False) def _getNumWorkspaceStructures(self): """ Return the number of structures that are included in the workspace. Note that scratch structures are ignored. """ try: proj = maestro.project_table_get() except project.ProjectException: # We're in the middle of opening or closing a project return 0 num_strucs = len(proj.included_rows) return num_strucs def _pickAtomRbToggled(self): """ When the user choose "Use this atom" or "Use pKa atoms from the project table", update the picking check boxes. Also create pKa properties in the project table if necessary. """ self._updatePickWarningAndCheckBoxes() ui = self.ui if ui.use_this_atom_rb.isChecked() and ui.pick_this_atom_cb.isEnabled(): ui.pick_this_atom_cb.setChecked(True) elif (ui.use_proj_table_atom_rb.isChecked() and ui.pick_proj_table_atom_cb.isEnabled()): ui.pick_proj_table_atom_cb.setChecked(True) self._createPkaProperties()
[docs] def pickThisAtom(self, atom_num): """ Respond to the atom picking at atom in the "Use this atom" section. :param atom_num: The atom number that was just picked :type atom_num: int """ struc = maestro.workspace_get() atom = struc.atom[atom_num] atom_name = self._getAtomName(atom, struc) self.ui.pick_this_atom_le.setText(atom_name)
[docs] def pickProjTableAtom(self, atom_num): """ Respond to the atom picking at atom in the "Use pKa atoms from the project table" section. :param atom_num: The atom number that was just picked :type atom_num: int """ workspace_struc = maestro.workspace_get() atom = workspace_struc.atom[atom_num] proj = maestro.project_table_get() entry_id = atom.entry_id try: row = proj.getRow(entry_id) except ValueError: # Scratch atoms have entry IDs starting with "Scratch", which # causes getRow() to raise a ValueError. self.error("Scratch atom selected.") return atom_name = self._getAtomName(atom, workspace_struc) row[self.PKA_ATOM_PROP] = atom_name proj.update()
def _getAtomName(self, atom, struc): """ Get the atom name for the specified atom after applying the jaguar naming scheme to the structure. :param atom: The atom to get the name of :type atom: `schrodinger.structure._StructureAtom` :param struc: The structure containing atom :type atom: `schrodinger.structure.Structure` :return: The atom name :rtype: str """ jaginput.apply_jaguar_atom_naming(struc) return atom.name def _getPkaAtom(self): """ Return the currently selected pKa atom """ if self.ui.use_this_atom_rb.isChecked(): atom_text = self.ui.pick_this_atom_le.text() return atom_text else: struc = self._getStructure() try: atom_name = struc.property[self.PKA_ATOM_PROP] return atom_name except KeyError: return ""
[docs] def getMmJagKeywords(self): keywords = super(MoleculeTabPka, self).getMmJagKeywords() keywords[mm.MMJAG_SKEY_IPKAT] = self._getPkaAtom() return keywords
[docs] def validate(self): """ Make sure that a pka atom has been selected and is a valid atom for the currently selected structure """ pka_atom = self._getPkaAtom() if not pka_atom: return "No pKa atom selected" # Note that this function will never get called if no structure is # selected, since the input selector will fail validation first struc = self._getStructure() jaginput.apply_jaguar_atom_naming(struc) matching_atoms = [atom for atom in struc.atom if atom.name == pka_atom] if not matching_atoms: return "No pKa atom %s in selected structure" % pka_atom elif len(matching_atoms) >= 2: return "Multiple atoms named %s in selected structure" % pka_atom
[docs] def loadSettings(self, jag_input): super(MoleculeTabPka, self).loadSettings(jag_input) self.ui.use_this_atom_rb.setChecked(True) pka_atom = jag_input[mm.MMJAG_SKEY_IPKAT] self.ui.pick_this_atom_le.setText(pka_atom)
def _createPkaProperties(self): """ Create a pKa property in the project table for the structures that are currently selected in the input selector. Note that this function has no effect if Files is selected. """ in_sel = self.input_selector if in_sel.inputState() == in_sel.FILE: return proj = maestro.project_table_get() if in_sel.inputState() == in_sel.SELECTED_ENTRIES: rows = proj.selected_rows elif in_sel.inputState() == in_sel.INCLUDED_ENTRIES: rows = proj.included_rows for cur_row in rows: if not cur_row[self.PKA_ATOM_PROP]: cur_row[self.PKA_ATOM_PROP] = "" proj.update() maestro.command("showpanel table")
[docs] def isZwitterionChecked(self): return self.ui.zwitterion_cb.isChecked()
[docs] def isConformationalSearchesChecked(self): return self.ui.conf_searches_cb.isChecked()
[docs]class MoleculeTabNoInputSelector(MoleculeTab): """ A Molecule tab that does not use the input selector """
[docs] def setup(self): self._struc = None super(MoleculeTabNoInputSelector, self).setup()
[docs] def setStructure(self, struc): """ Change the structure that this tab is representing :param struc: The structure :type struc: `schrodinger.structure.Structure` """ self._struc = struc self.ui.basis_selector.structureChanged(struc) self._fixSpinAndUpdateWarning()
[docs] def structureUpdated(self): """ React to a change in the structure described by self._struc. i.e. The `schrodinger.structure.Structure` object passed to `setStructure` has not changed, but the structure described by that object has. """ self.ui.basis_selector.structureUpdated() self._fixSpinAndUpdateWarning()
def _getNumStructures(self): return int(self._struc is not None) def _getStructure(self): return self._struc # _getStructure is private in the super class, but it should be public for # this subclass getStructure = _getStructure
[docs] def createChargeProperties(self): if not maestro: err = ("Project table properties may not be created outside of " "Maestro.") self.error(err) return struc = self._getStructure() if struc is None: self.error("No structure selected.") return try: entry_id = struc.property["s_m_entry_id"] except ValueError: err = ("Project table properties can only be created for " "structures from the project table") self.error(err) return esl = "entry %s" % entry_id self._createChargePropertiesForEsl(esl)