Source code for schrodinger.application.matsci.appbase

"""
Module for customizing af2 app features

Copyright Schrodinger, LLC. All rights reserved.
"""
import glob
import os

import schrodinger
from schrodinger import structure
from schrodinger.application.matsci import msprops
from schrodinger.application.matsci import jobutils
from schrodinger.project import utils as proj_utils
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.ui.qt import filedialog
from schrodinger.ui.qt import swidgets
from schrodinger.ui.qt import utils as qt_utils
from schrodinger.ui.qt.appframework2 import af2
from schrodinger.utils import fileutils

maestro = schrodinger.get_maestro()


[docs]class MatSciAppMixin: """ General mixin for MatSci panels """ WAM_LOAD_SELECTED = af2.input_selector.InputSelector.SELECTED_ENTRIES WAM_LOAD_INCLUDED = af2.input_selector.InputSelector.INCLUDED_ENTRY
[docs] def initMixinOptions(self, wam_input_state=None, wam_load_function=None, wam_run_singleton=True, wam_set_panel_input_state=True): """ Initialize the options for the mixin :param str wam_input_state: The input state to use to get the WAM entries :param callable wam_load_function: Function that takes no arguments and loads the entries into the panel. :param bool wam_run_singleton: Whether the panel singleton should be run (i.e. displayed) or not. :param bool wam_set_panel_input_state: Whether to set panel's input_selector state to the corresponding wam_input_state """ assert wam_input_state in (None, self.WAM_LOAD_SELECTED, self.WAM_LOAD_INCLUDED) self._wam_input_state = wam_input_state self._wam_load_function = wam_load_function self._wam_run_singleton = wam_run_singleton self._wam_set_panel_input_state = wam_set_panel_input_state
[docs] @classmethod def panel(cls, *entry_ids): """ Launch a singleton instance of the panel and load entries if applicable. See `af2.App.panel` for more information. :param tuple entry_ids: Entry ids to load into the panel :raise RuntimeError: If the input state is invalid """ if not entry_ids: return super().panel() the_panel = super().panel(run=False) if not hasattr(the_panel, '_wam_run_singleton'): the_panel.error( '"initMixinOptions" has not been called by the panel. Could' ' not open panel for entries.') return the_panel if the_panel._wam_run_singleton: the_panel.run() input_state = the_panel._wam_input_state if input_state is None: return the_panel if input_state == cls.WAM_LOAD_INCLUDED: command = 'entrywsincludeonly entry ' + str(entry_ids[0]) elif input_state == cls.WAM_LOAD_SELECTED: command = 'entryselectonly entry ' + ' '.join(map(str, entry_ids)) if the_panel._wam_set_panel_input_state: for selector in ('input_selector', '_if'): input_selector = getattr(the_panel, selector, None) if input_selector: the_panel.input_selector.setInputState(input_state) break maestro.command(command) if the_panel._wam_load_function: the_panel._wam_load_function() return the_panel
[docs]class ProcessPtStructuresApp(af2.App): """ Base class for panels that process pt structures and either replace them or creates new entries """ REPLACE_ENTRIES = 'Replace current entries' NEW_ENTRIES = 'Create new entries' RUN_BUTTON_TEXT = 'Run' # Can be overwritten for custom button name TEMP_DIR = fileutils.get_directory_path(fileutils.TEMP) # Used for unittests. Should be overwritten in derived classes. DEFAULT_GROUP_NAME = 'new_structures'
[docs] def setPanelOptions(self): """ Override the generic parent class to set panel options """ super().setPanelOptions() self.input_selector_options = { 'file': False, 'selected_entries': True, 'included_entries': True, 'included_entry': False, 'workspace': False }
[docs] def layOut(self): """ Lay out the widgets for the panel """ super().layOut() layout = self.main_layout # Any widgets for subclasses should be added to self.top_main_layout self.top_main_layout = swidgets.SVBoxLayout(layout=layout) output_gb = swidgets.SGroupBox("Output", parent_layout=layout) self.output_rbg = swidgets.SRadioButtonGroup( labels=[self.REPLACE_ENTRIES, self.NEW_ENTRIES], layout=output_gb.layout, command=self.outputTypeChanged, nocall=True) hlayout = swidgets.SHBoxLayout(layout=output_gb.layout, indent=True) dator = swidgets.FileBaseNameValidator() self.group_name_le = swidgets.SLabeledEdit( "Group name: ", edit_text=self.DEFAULT_GROUP_NAME, validator=dator, always_valid=True, layout=hlayout) self.outputTypeChanged() layout.addStretch() self.status_bar.showProgress() self.app = QtWidgets.QApplication.instance() # MATSCI-8244 size_hint = self.sizeHint() size_hint.setWidth(410) self.resize(size_hint) # Set custom run button name if subclass defines it self.bottom_bar.start_bn.setText(self.RUN_BUTTON_TEXT)
[docs] def outputTypeChanged(self): """ React to a change in output type """ self.group_name_le.setEnabled( self.output_rbg.checkedText() == self.NEW_ENTRIES)
[docs] @af2.appmethods.start() def myStartMethod(self): """ Process the selected or included rows' structures """ # Get rows ptable = maestro.project_table_get() input_state = self.input_selector.inputState() if input_state == self.input_selector.INCLUDED_ENTRIES: rows = [row for row in ptable.included_rows] elif input_state == self.input_selector.SELECTED_ENTRIES: rows = [row for row in ptable.selected_rows] # Prepare progress bar nouser = QtCore.QEventLoop.ExcludeUserInputEvents num_structs = len(rows) self.progress_bar.setValue(0) self.progress_bar.setMaximum(num_structs) self.app.processEvents(nouser) structs_per_interval = max(1, num_structs // 20) # Initialize output means if self.output_rbg.checkedText() == self.REPLACE_ENTRIES: modify = True writer = None else: modify = False file_path = os.path.join(self.TEMP_DIR, self.group_name_le.text() + ".mae") writer = structure.StructureWriter(file_path) self.setUp() # Process the structures with qt_utils.wait_cursor: for index, row in enumerate(rows, start=1): with proj_utils.ProjectStructure(row=row, modify=modify) as \ struct: try: passed = self.processStructure(struct) if passed and writer: writer.append(struct) except WorkflowError: break if not index % structs_per_interval: self.progress_bar.setValue(index) self.app.processEvents(nouser) # Import file if applicable if writer: writer.close() if os.path.exists(file_path): # No file will exist if all sts fail ptable = maestro.project_table_get() ptable.importStructureFile(file_path, wsreplace=True, creategroups='all') fileutils.force_remove(file_path) # Show 100%. Needed when num_structs is large and not a multiple of 20 self.progress_bar.setValue(num_structs) self.app.processEvents(nouser) # Panel-specific wrap up self.wrapUp()
[docs] def setUp(self): """ Make any preparations required for processing structures """ pass
[docs] def processStructure(self, struct): """ Process each structure. Should be implemented in derived classes. :param `structure.Structure` struct: The structure to process """ raise NotImplementedError
[docs] def wrapUp(self): """ Wrap up processing the structures """ pass
[docs] def reset(self): """ Reset the panel """ self.group_name_le.reset() self.output_rbg.reset() self.outputTypeChanged() self.progress_bar._bar.reset( ) # af2.ProgressFrame doesn't have a reset method
[docs]class WorkflowError(ValueError): """ Custom exception for when the workflow should be stopped """ pass
[docs]class BaseAnalysisGui(object): """ Base class for other mixin gui class in the module """
[docs] def getIncludedEntry(self): """ Get included entry in maestro :return `schrodinger.structure.Structure`: Structure in workspace """ # Tampering with licensing is a violation of the license agreement if not jobutils.check_license(panel=self): return try: struct = maestro.get_included_entry() except RuntimeError as msg: self.error(str(msg)) return return struct
[docs] def resetPanel(self): """ Reset the panel variables and widgets set by this mixin """ self.struct = None self.toggleStateMain(False) self.load_btn.reset()
[docs] def toggleStateMain(self, state): """ Class to enable/disable widgets in panel when structure is added/removed. :param bool state: If True it will enable the panel and will disable if false :raise NotImplementedError: Will raise error if it is not overwritten. """ raise NotImplementedError( 'toggleStateMain method should be implemented ' 'to enable/disable panel widgets.')
[docs]class StructureLoadButton(swidgets.SPushButton): """ Class to load workspace structure to the panel """ LOAD_FROM_WORKSPACE = 'Load from Workspace' NO_STRUCT_LABEL = 'No structure loaded' MAX_TITLE_LENGTH = 20
[docs] def __init__(self, layout, command, label=LOAD_FROM_WORKSPACE, no_struct_label=NO_STRUCT_LABEL): """ Load structure button along with elided title next to it. :param `QLayout` layout: layout to add the button to :param function command: function to load structure. Function should return structure. :param str label: Label for the structure load button :param str no_struct_label: title when no structure is loaded """ self.command = command load_layout = swidgets.SHBoxLayout(layout=layout) self.no_struct_label = no_struct_label super().__init__(label, layout=load_layout, command=self.loadStruct) self.struct_label = swidgets.SLabel(no_struct_label, layout=load_layout) load_layout.addStretch() self.mylayout = load_layout
[docs] def loadStruct(self): """ Load structure using the provided command and set the title as label """ struct = self.command() if not struct: return # Set structure label title = self.getElidedTitle(struct.title) self.struct_label.setText(title) # Set structure tool tip etitle = struct.title eid = struct.property[msprops.ENTRY_ID_PROP] tip = f'{etitle} (Entry ID: {eid})' self.struct_label.setToolTip(tip)
[docs] def getElidedTitle(self, title): """ Return right elided string for according to the maximum length :return str: Will return right elided text for the passed title if longer than the maximum length else will return the title """ if len(title) > self.MAX_TITLE_LENGTH: title = title[:self.MAX_TITLE_LENGTH] + '...' return title
[docs] def reset(self): """ Reset the title label """ self.struct_label.reset()
[docs]class ViewerGuiMixin(MatSciAppMixin, BaseAnalysisGui): """ Class for extension of af2 to add widgets to gui for calculation viewer """
[docs] def addLoadStructButton(self, layout, file_endings, filename_props=None): """ Load button to load structure from workspace and associated data file :param `QLayout` layout: layout to add the button to :param list file_endings: The strings at the end of the data file paths, including the extensions :param list filename_props: The structure property which contain data property :raise NotImplementedError: If panel_id was not set in the __init__ before adding the load structure button :raise ValueError: If filename_props is not None and the length of file endings is not equal to it. """ self.file_endings = file_endings self.filename_props = filename_props self.load_btn = StructureLoadButton(command=self._importStructData, layout=layout) self.panel_id = 'saved_file' if not hasattr( self, 'panel_id') else self.panel_id self.initMixinOptions(wam_input_state=self.WAM_LOAD_INCLUDED, wam_load_function=self.load_btn.loadStruct)
[docs] def setFilesUsingExt(self, path): """ Sets the data files using extension :param path: The source/job path :type path: str """ if not self.file_endings: return for file_ending in self.file_endings: if not path or not os.path.exists(path): filenames = [] else: gen_filename = '*' + file_ending filenames = glob.glob(os.path.join(path, gen_filename)) if len(filenames) == 1: self.data_files[file_ending] = filenames[0] continue self.warning('The job directory for the workspace structure ' 'could not be found. Please select the ' f'"*{file_ending}" data file.') file_filter = f'Data file (*{file_ending})' filename = filedialog.get_open_file_name(self, filter=file_filter, id=self.panel_id) if not filename: # User canceled hence clear all the data files collected self.data_files = {} return self.data_files[file_ending] = filename # Update dir for next file path = os.path.dirname(filename)
[docs] def setFilesUsingStProp(self, path): """ Sets the data files using structure properties :param path: The source/job path :type path: str """ if not self.filename_props: return None for st_prop in self.filename_props: # If the property is not loaded on the structure, then don't bother # looking for the corresponding file expected_file = self.struct.property.get(st_prop) if not expected_file: continue # Get full path to the file file_path = os.path.join(path, expected_file) # Check for the file path, if the file is not found as user for # for the file and update the source path location if os.path.isfile(file_path): filename = file_path else: self.warning( f'The data file {expected_file} for property "{st_prop}" ' 'could not be found') file_ext = fileutils.splitext(expected_file)[-1] file_filter = f'Data file (*{file_ext})' filename = filedialog.get_open_file_name(self, filter=file_filter, id=self.panel_id) if not filename: # User canceled hence clear all the data files collected self.data_files = {} return # Update dir for next file path = os.path.dirname(filename) self.data_files[st_prop] = filename if not self.data_files: self.warning( f'No appropriate data files found for {self.struct.title}')
def _setDataFiles(self): """ Set the paths for the data files needed to setup the panel """ path = jobutils.get_source_path(self.struct) self.setFilesUsingExt(path) self.setFilesUsingStProp(path) def _importStructData(self): """ Load the workspace file and data. Note create loadData method to load the data :return `schrodinger.structure.Structure`: Structure that was loaded """ self.resetPanel() self.struct = self.getIncludedEntry() if not self.struct: return # Get the data file self._setDataFiles() if not self.data_files: return if self.loadData() is False: return self.toggleStateMain(True) return self.struct
[docs] def loadData(self): """ Class to load data to the panel :raise NotImplementedError: Will raise error if it is not overwritten. :rtype: bool or None :return: False means the setup failed. True or None means it succeeded. """ raise NotImplementedError('loadData method should be implemented to ' 'load results into the panel.')
[docs] def resetPanel(self): """ Reset the panel variables and widgets set by this mixin """ super().resetPanel() self.data_files = {}