import schrodinger
from schrodinger import project
from schrodinger.application.jaguar import input as jaginput
from schrodinger.infra import mm
from schrodinger.ui.qt import entryselector
from .base_tab import BaseTab
from .base_tab import ProvidesBasisMixin
from .base_tab import ProvidesStructuresMixin
maestro = schrodinger.get_maestro()
[docs]class MultiStructureTab(ProvidesBasisMixin, ProvidesStructuresMixin, BaseTab):
"""
A parent class for tabs that allow transition state, reactant, and product
structures to be selected. MultiStructureTabs also contain a Molecule
subtab. Note that this class is not intended to be directly instantiated
and instead should be subclassed (such as the IRC tab or Transition State
tabs).
"""
ALL_STRUCS = list(range(3))
TRANSITION_STATE, REACTANT, PRODUCT = ALL_STRUCS
ENTRY_TYPE_NAMES = ("transition state", "reactant", "product")
STRUC_DIFF_WARNING = ("Warning: The %s and %s\n"
"entries contain different %s. This must\n"
"be resolved before running the job.")
ATOMS = "atoms"
ATOM_LABELS = "atom labels"
[docs] def setup(self):
self.ui.transition_state_btn.clicked.connect(
lambda: self.chooseEntry(self.TRANSITION_STATE))
self.ui.reactant_btn.clicked.connect(
lambda: self.chooseEntry(self.REACTANT))
self.ui.product_btn.clicked.connect(
lambda: self.chooseEntry(self.PRODUCT))
self.ui.transition_state_cb.toggled.connect(
lambda: self.structureCbToggled(self.TRANSITION_STATE))
self.ui.reactant_cb.toggled.connect(
lambda: self.structureCbToggled(self.REACTANT))
self.ui.product_cb.toggled.connect(
lambda: self.structureCbToggled(self.PRODUCT))
self.struc_les = [
self.ui.transition_state_le, self.ui.reactant_le, self.ui.product_le
]
self.struc_cbs = [
self.ui.transition_state_cb, self.ui.reactant_cb, self.ui.product_cb
]
self.basis_changed = self.ui.molecule_sub_tab.basis_changed
self.reset()
[docs] def reset(self):
"""
Clear the stored structures
"""
self._entry_ids = [None] * 3
self._updateStrucListings()
self.ui.molecule_sub_tab.reset()
[docs] def setStructureSelectorsEnabled(self, transition_state, reactant, product):
"""
Enable or disable the three structure selection rows
"""
self.ui.transition_state_cb.setEnabled(
transition_state and
self._entry_ids[self.TRANSITION_STATE] is not None)
self.ui.transition_state_lbl.setEnabled(transition_state)
self.ui.transition_state_le.setEnabled(transition_state)
self.ui.transition_state_btn.setEnabled(transition_state)
self.ui.reactant_cb.setEnabled(
reactant and self._entry_ids[self.REACTANT] is not None)
self.ui.reactant_lbl.setEnabled(reactant)
self.ui.reactant_le.setEnabled(reactant)
self.ui.reactant_btn.setEnabled(reactant)
self.ui.product_cb.setEnabled(product and
self._entry_ids[self.PRODUCT] is not None)
self.ui.product_lbl.setEnabled(product)
self.ui.product_le.setEnabled(product)
self.ui.product_btn.setEnabled(product)
self._updateStructureWarningLabel()
[docs] def chooseEntry(self, struc_type):
"""
Open the EntrySelector dialog for the specified structure and save the
selected entry.
:param struc_type: The structure to select. Must be one of
self.TRANSITION_STATE, self.REACTANT, or self.PRODUCT.
:type struc_type: int
"""
entry = entryselector.get_entry(self)
if entry is not None:
self._entry_ids[struc_type] = entry
self._updateStrucListings()
self.structureChanged.emit()
def _updateStrucListings(self):
"""
Update the contents and status of all structure selection widgets based
on the contents of self._entry_ids and the structures currently loaded
into the project table.
"""
proj = maestro.project_table_get()
for struc_type in self.ALL_STRUCS:
eid = self._entry_ids[struc_type]
line_edit = self.struc_les[struc_type]
row = self._getRow(proj, eid)
if row is None:
self._entry_ids[struc_type] = None
line_edit.setText("")
self.struc_cbs[struc_type].setEnabled(False)
self.struc_cbs[struc_type].setChecked(False)
else:
title = row.title
line_edit.setText("%s:%s" % (eid, title))
in_workspace = (row.in_workspace != project.NOT_IN_WORKSPACE)
enabled = self.struc_les[struc_type].isEnabled()
self.struc_cbs[struc_type].setEnabled(enabled)
self.struc_cbs[struc_type].setChecked(in_workspace)
self._updateStructureWarningLabel()
self._updateMoleculeSubTabStruc()
def _getRow(self, proj, eid):
"""
Retrieve the specified row from the project table. If there is no such
row, return None.
:param proj: The maestro project
:type proj: `schrodinger.project.Project`
:param eid: The entry ID to retrieve
:type eid: str
:return: The requested row, or None if there is no such row.
:rtype: `schrodinger.project.ProjectRow` or NoneType
"""
if eid is None:
return None
try:
return proj.getRow(eid)
except (ValueError, mm.MmException):
return None
def _updateMoleculeSubTabStruc(self):
"""
Update the Molecule sub-tab structure. This structure is used for basis
set calculations.
"""
representative_struc = self.getRepresentativeStructure()
self.ui.molecule_sub_tab.setStructure(representative_struc)
[docs] def projectUpdated(self):
"""
Update the listing of structures when the project is updated. Note that
the callback for this function is registered in the
base_panel.MultiStructureMixin class so that it can be unregistered
when the panel is closed.
"""
try:
self._updateStrucListings()
except project.ProjectException:
pass
def _updateStructureWarningLabel(self):
"""
Update the label that warns about mismatched structures
"""
diff = self._compareStrucs()
if diff is None:
text = ""
else:
text = self.STRUC_DIFF_WARNING % diff
self.ui.struc_warning_lbl.setText(text)
[docs] def validate(self):
"""
Make sure that all of the appropriate structures are loaded and that
they all have the same atoms.
"""
for entry_type in self.ALL_STRUCS:
if (self.struc_les[entry_type].isEnabled() and
self._entry_ids[entry_type] is None):
return ("No structure loaded for %s." %
self.ENTRY_TYPE_NAMES[entry_type])
diff = self._compareStrucs()
if diff is not None:
return "The %s and %s entries contain different %s." % diff
basis_selector_err = self.ui.molecule_sub_tab.validate()
if basis_selector_err:
return basis_selector_err
[docs] def structureCbToggled(self, struc_type):
"""
Respond to the specified structure inclusion checkbox being toggled by
including or excluding the relevant structure from the workspace.
"""
eid = self._entry_ids[struc_type]
if eid is None:
return
if self.struc_cbs[struc_type].isChecked():
include = project.IN_WORKSPACE
else:
include = project.NOT_IN_WORKSPACE
proj = maestro.project_table_get()
proj.getRow(eid).in_workspace = include
def _compareStrucs(self):
"""
Compare all structures loaded into this tab to make sure that they have
the same atoms. Two structures are the same if they:
- Have the same number of atoms
- Have atoms with the same elements in the same order
- Have atoms with the same names after Jaguar atom naming is applied
:return: None if all structures are the same. Otherwise, returns a
tuple of:
- The name of the first structure that's different
- The name of the second structure that's different
- What's different about the structures. This will be "atoms" if
there are a different number of atoms or atoms with different
elements, and it will be "atom labels" if there are atoms with
different names after Jaguar atom naming is applied.
:rtype: NoneType or tuple
"""
entry_types, elems, atom_names = self._getAtomicNumbersAndAtomNames()
return self._compareAtomicNumbersAndAtomNames(entry_types, elems,
atom_names)
def _getAtomicNumbersAndAtomNames(self):
"""
Retrieve the atomic numbers and atom names for all structures that are
relevant to the current job type (i.e. for all rows that are populated
and enabled)
:return: A tuple of:
- A list of entry types for all structures are relevant
- A list of [atomic numbers for the first relevant structure,
atomic numbers for the second relevant structure, ...]
- A list of [atom names for the first relevant structure,
atom names for the second relevant structure, ...]
:rtype: tuple
"""
entry_types = []
elems = []
atom_names = []
proj = maestro.project_table_get()
for entry_type in self.ALL_STRUCS:
struc = self._getStruc(proj, entry_type)
if struc is None:
continue
jaginput.apply_jaguar_atom_naming(struc)
non_dummy_atoms = [
atom for atom in struc.atom if atom.atomic_number != -2
]
cur_elems = [atom.atomic_number for atom in non_dummy_atoms]
cur_atom_names = [atom.name for atom in non_dummy_atoms]
entry_types.append(entry_type)
elems.append(cur_elems)
atom_names.append(cur_atom_names)
return entry_types, elems, atom_names
def _compareAtomicNumbersAndAtomNames(self, entry_types, elems, atom_names):
"""
Compare the provided atomic numbers and atom names and return a
description of the differences
:param entry_types: A list of entry types for all structures are
relevant
:type entry_types: list
:param elems: A list of [atomic numbers for the first relevant
structure, atomic numbers for the second relevant structure, ...]
:type elems: list
:param atom_names: A list of [atom names for the first relevant
structure, atom names for the second relevant structure, ...]
:type atom_names: list
:return: None if all structures are the same. Otherwise, returns a
tuple of:
- The name of the first structure that's different
- The name of the second structure that's different
- What's different about the structures ("atoms" or "atom labels")
:rtype: NoneType or tuple
"""
diff = None
if len(elems) > 1:
if elems[0] != elems[1]:
diff = [0, 1, self.ATOMS]
elif atom_names[0] != atom_names[1]:
diff = [0, 1, self.ATOM_LABELS]
if len(elems) > 2:
if elems[0] != elems[2]:
diff = [0, 2, self.ATOMS]
elif atom_names[0] != atom_names[2]:
diff = [0, 2, self.ATOM_LABELS]
if diff is not None:
diff[0] = self.ENTRY_TYPE_NAMES[entry_types[diff[0]]]
diff[1] = self.ENTRY_TYPE_NAMES[entry_types[diff[1]]]
diff = tuple(diff)
return diff
[docs] def getStructures(self):
"""
Get all of the structures that are loaded into the tab. Each structure
will be a `schrodinger.structure.Structure` object containing the
loaded structure, or None if no structure has been loaded.
:return: A list of:
- the transition state structure
- the reactant structure
- the product structure
:rtype: list
"""
proj = maestro.project_table_get()
strucs = [
self._getStruc(proj, entry_type) for entry_type in self.ALL_STRUCS
]
return strucs
def _getStruc(self, proj, entry_type):
"""
Retrieve the specified structure
:param proj: The current project
:type proj: `schrodinger.project.Project`
:param struc_type: The structure to retrieve. Must be one of
self.TRANSITION_STATE, self.REACTANT, or self.PRODUCT.
:type struc_type: int
"""
eid = self._entry_ids[entry_type]
if eid is not None and self.struc_les[entry_type].isEnabled():
cur_struc = proj.getRow(eid).getStructure()
return cur_struc
else:
return None
[docs] def setStructures(self, entry_ids, jag_input=None):
"""
Load the specified entry IDs into the tab
:param entry_ids: A list of:
- the transition state structure entry ID
- the reactant structure entry ID
- the product structure entry ID
Note that this value must be a list, not a tuple.
:type entry_ids: list
:param jag_input: A JaguarInput object containing the job settings.
Note that this argument is only used for the Transition State tab
version of this function in order to determine if the search method
is LST, as this will affect how the structures are loaded.
:type jag_input: `schrodinger.application.jaguar.input.JaguarInput`
"""
self._entry_ids = entry_ids
self._updateStrucListings()
[docs] def getRepresentativeStructure(self):
"""
Retrieve one structure that can be used for basis selector calculations.
:return: A single representative structure, or None if no structures are
loaded.
:rtype: `schrodinger.structure.Structure` or NoneType
"""
try:
proj = maestro.project_table_get()
except project.ProjectException:
return None
for entry_type in self.ALL_STRUCS:
cur_struc = self._getStruc(proj, entry_type)
if cur_struc is not None:
return cur_struc
[docs] def getEids(self):
"""
Get the entry ids loaded into the tab
:return: A tuple of
- A list of entry ids loaded into the tab
- Whether the structures have matching atoms and atom names (bool)
:rtype: tuple
"""
eids = [eid for eid in self._entry_ids if eid is not None]
diff = self._compareStrucs()
acceptable = diff is None
return (eids, acceptable)
[docs] def getStructureTitleForJobname(self):
# See ProvidesStructuresMixin for method documentation
struc = self.getRepresentativeStructure()
if struc is None:
return None
else:
return struc.title
# The following functions simply call the equivalent Molecule sub-tab
# function
[docs] def getDefaultKeywords(self):
return self.ui.molecule_sub_tab.getDefaultKeywords()
[docs] def getMmJagKeywords(self):
return self.ui.molecule_sub_tab.getMmJagKeywords()
[docs] def getBasis(self, ignored=None):
return self.ui.molecule_sub_tab.getBasis()
[docs] def loadSettings(self, jag_input):
ret = self.ui.molecule_sub_tab.loadSettings(jag_input)
self.structureChanged.emit()
return ret