"""
GUI elements to aid in pulling dimers or clusters of nearest
neighbors from a larger structure.
Copyright Schrodinger, LLC. All rights reserved.
"""
from schrodinger.application.matsci import clusterstruct
from schrodinger.application.matsci import gutils
from schrodinger.application.matsci import rdpattern
from schrodinger.Qt import QtCore
from schrodinger.ui.qt import swidgets
CRITERIA = [
clusterstruct.NO_MOLS, clusterstruct.ONE_ONLY_MOL,
clusterstruct.AT_LEAST_ONE_MOL, clusterstruct.TWO_MOLS
]
[docs]class SpeciesCombo(swidgets.SLabeledComboBox):
"""
Combobox for showing species
"""
[docs] def __init__(self, label=None, **kwargs):
"""
Create a SpeciesCombo instance
:param str label: The label for the combobox
"""
super().__init__(label, **kwargs)
if not label:
self.label.hide()
self._fixTypeComboSizing()
def _fixTypeComboSizing(self):
"""
Make sure the combo expands if necessary when new species are added
"""
self.setSizeAdjustPolicy(self.AdjustToContents)
policy = self.sizePolicy()
policy.setHorizontalPolicy(policy.Minimum)
self.setSizePolicy(policy)
[docs] def setCurrentSpecies(self, species):
"""
Fill the combo with the list of current species
:type species: dict
:param species: Keys are unique SMILES strings, values are SpeciesData
objects for the species with that SMILES string.
"""
self.clear()
itemdict = clusterstruct.get_species_display_names(species.values())
self.addItemsFromDict(itemdict)
[docs] def currentSpecies(self):
"""
Return the currently selected species
:rtype: clusterstruct.SpeciesData
:return: The currently selected species
"""
return self.currentData()
[docs] def getSampleMolNumber(self):
"""
Get a sample molecule number for the current species
:rtype: int
:return: a sample molecule number for the current species
"""
return self.currentSpecies().getSampleMolNumber()
[docs]class MonomerSpeciesSelector(swidgets.SCheckBoxWithSubWidget):
"""
Checkbox and combo that allows the user to select a species. The enabled
state of the combobox is controlled by the checkbox.
"""
species_changed = QtCore.pyqtSignal()
[docs] def __init__(self,
label='Limit active molecules to those of type:',
command=None,
find_species_fn=None,
**kwargs):
"""
Create a Monomer SpeciesSelector instance
:type label: str
:param label: The text between the checkbox and combobox
:type command: callable
:param command: The slot to connect to the species_changed signal
:type find_species_fn: function
:param find_species_fn: Function that is used to create species
All other keyword args are passed to the SCheckBoxWithSubWidget class
"""
self.species = {}
self.find_species_fn = (find_species_fn if find_species_fn else
clusterstruct.find_species)
self.subframe = swidgets.SFrame(layout_type=swidgets.HORIZONTAL)
self.createSubWidgets()
swidgets.SCheckBoxWithSubWidget.__init__(self,
label,
self.subframe,
checked=False,
**kwargs)
if command:
self.species_changed.connect(command)
[docs] def currentSpecies(self):
"""
Return the currently selected species
:rtype: clusterstruct.SpeciesData
:return: The currently selected species
"""
return self.type_combo.currentSpecies()
[docs] def speciesChanged(self):
"""
React to a new species being selected
"""
self.species_changed.emit()
[docs] def hasSpecies(self):
"""
Check if any species have been loaded
:rtype: bool
:return: True of any species has been loaded, False if not
"""
return bool(self.type_combo.count())
[docs] def clear(self):
"""
Clear out the species combo
"""
self.type_combo.clear()
[docs] def loadSpecies(self, structs):
"""
Find all the species in the given structures and load them into the
species type combobox
:type structs: list(structure.Structure)
:param structs: The structures to find the species in
:rtype: list(clusterstruct.SpeciesData)
:return: Each item of the list is the data for a species found
"""
species = self.find_species_fn(structs)
self.setCurrentSpecies(species)
return species
[docs] def setCurrentSpecies(self, species):
"""
Fill the species combo with the list of current species
:type species: dict
:param species: Keys are unique SMILES strings, values are SpeciesData
objects for the species with that SMILES string.
"""
self.species = species
self.type_combo.setCurrentSpecies(species)
[docs] def reset(self):
"""
Reset the widget, including clearing the species type combo
"""
swidgets.SCheckBoxWithSubWidget.reset(self)
self.clear()
[docs] def getNumberOfType(self):
"""
Get the number of members of the current species
:rtype: int
:return: The number of members of the current species
"""
data = self.type_combo.currentData()
if data:
return len(data.members)
else:
return 0
[docs]class DimerSpeciesSelector(MonomerSpeciesSelector):
"""
Checkbox and combo that allows the user to select a species for the purpose
of selecting dimers with or without the given species. The enabled state of
the combobox is controlled by the checkbox.
"""
criterion_changed = QtCore.pyqtSignal(str)
[docs] def __init__(self,
label='Limit dimers to only those that contain:',
command=None,
**kwargs):
"""
Create a DimerSpeciesSelector instance
:type label: str
:param label: The text between the checkbox and combobox
:type command: callable
:param command: The slot to connect to the species_changed and
criterion_changed signals
See parent class for additional documentation
"""
MonomerSpeciesSelector.__init__(self,
label=label,
command=command,
**kwargs)
if command:
self.criterion_changed.connect(command)
[docs] def currentCriterion(self):
"""
Return the current amount criterion
:rtype: str
:return: The desired amount of the species. Will be one of the items
from the CRITERIA list
"""
return self.crit_combo.currentText()
[docs] def criterionChanged(self):
"""
React to a change in the amount criterion
"""
self.criterion_changed.emit(self.currentCriterion())
[docs] def reset(self):
"""
Reset the widgets
"""
MonomerSpeciesSelector.reset(self)
self.crit_combo.reset()
[docs] def applyToDimers(self, dimers):
"""
Apply the current settings to a list of dimers, marking them as meeting
the criteria or not. If the controlling checkbox is unchecked, all
dimers will be marked as meeting the criteria.
:type dimers: list
:param dimers: A list of Dimer objects. Each dimer will have its
meets_species_criterion property set based on the results of the
Dimer.evaluateSpeciesCriterion method.
"""
if self.isChecked():
crit_species = self.currentSpecies()
crit = self.currentCriterion()
species = self.species
else:
crit_species = crit = species = None
for dimer in dimers:
dimer.evaluateSpeciesCriterion(crit_species, crit, species)
[docs]def count_species_in_cms(model, *, sanitize=True, include_stereo=True):
"""
Enumerates and counts the species in a Desmond system.
:param cms.Cms model: The model you want to count the species for
:param bool sanitize: Whether RDKit sanitization should be performed when
identifying unique species. This option is not applicable for
coarsegrained structures.
:param bool include_stereo: Whether the stereochemistry of the structure
should be considered when identifying unique species. Setting to
`False` can speed this up substantially.
:rtype: dict
:return: Dictionary whose keys are the display formulae of the species and
whose values are the number of molecules of that species present in the
system.
"""
display_names = get_species_display_names_from_cms(
model, sanitize=sanitize, include_stereo=include_stereo)
counts = {}
for name, species in display_names.items():
counts[name] = len(species)
return counts
[docs]def get_species_display_names_from_cms(model,
*,
sanitize=True,
include_stereo=True):
"""
Gets formatted chemical formulae for each species from a Desmond system.
They are formatted for printing in a GUI.
:param cms.Cms model: The model you want to get the formulae for
:param bool sanitize: Whether RDKit sanitization should be performed when
identifying unique species to get chemical formulae for. This option is
not applicable for coarsegrained structures.
:param bool include_stereo: Whether the stereochemistry of the structure
should be considered when identifying unique species to analyze.
Setting to `False` can speed this up substantially.
:rtype: dict(str=clusterstruct.SpeciesData)
:return: Keys are the names of each species found in the system, and values
are their corresponding `SpeciesData` objects.
"""
# Find each species, and then sort it to get consistent ordering from this
# function. To speed this step up, you can set sanitize=True and
# stereo=False
pattern = rdpattern.Pattern(model.fsys_ct,
sanitize=sanitize,
include_stereo=include_stereo)
species_dict = clusterstruct.find_mol_species_from_rdmol([pattern])
all_species = [species for _, species in sorted(species_dict.items())]
display_names = _get_display_names_from_species(
all_species, is_cg=pattern.is_coarse_grain)
return display_names
def _get_display_names_from_species(species, *, is_cg=False):
"""
Turns a list of `SpeciesData` objects into a list of human-readable
chemical formulae meant for displaying in a GUI.
:param list(clusterstruct.SpeciesData) species: list of species you want to
get display names for
:param bool is_cg: whether all the species coarse-grained or not
:rtype: dict(str=clusterstruct.SpeciesData)
:return: Keys are the names of each species found in the system, and values
are their corresponding `SpeciesData` objects.
"""
display_names = clusterstruct.get_species_display_names(species)
subscripted_names = _subscript_species_display_names(display_names,
is_cg=is_cg)
# We've been bitten by de-synchronized species and names before. Check if
# it happens again. See MATSCI-12378
n_species = len(species)
n_names = len(subscripted_names)
if n_names != n_species:
msg = (f'Tried to find display names for {n_species} species, but was '
f'only able to make {n_names} names')
raise RuntimeError(msg)
return subscripted_names
def _subscript_species_display_names(display_names, *, is_cg=False):
"""
Turns a list of `SpeciesData` objects into a list of human-readable
chemical formulae meant for displaying in a GUI.
:param list(clusterstruct.SpeciesData) species: list of species you want to
get display names for
:param bool is_cg: whether all the species coarse-grained or not
:rtype: dict(str=clusterstruct.SpeciesData)
:return: Keys are the names of each species found in the system, and values
are their corresponding `SpeciesData` objects.
"""
# CG formulae look like (PEO)10 while AA formulae look like C6H6. We need
# to handle subscripting them differently.
if is_cg:
subscripting_function = gutils.subscript_cg_formula
else:
subscripting_function = gutils.subscript_chemical_formula
subscripted_display_names = {}
for display_name, species in display_names.items():
# Currently, display names are printed like "formula foo:bar baz". We
# only want to fix the formatting of the "formula" part.
split_name = display_name.split(' ')
formula = split_name[0]
suffix = ' '.join(split_name[1:])
subscripted_formula = subscripting_function(formula)
new_display_name = f'{subscripted_formula} {suffix}'
subscripted_display_names[new_display_name] = species
return subscripted_display_names