Source code for sas.qtgui.Utilities.TabbedModelEditor

# global
import sys
import os
import datetime
import logging
import traceback

from PyQt5 import QtWidgets, QtCore

from sas.sascalc.fit import models

import sas.qtgui.Utilities.GuiUtils as GuiUtils
from sas.qtgui.Utilities.UI.TabbedModelEditor import Ui_TabbedModelEditor
from sas.qtgui.Utilities.PluginDefinition import PluginDefinition
from sas.qtgui.Utilities.ModelEditor import ModelEditor

[docs]class TabbedModelEditor(QtWidgets.QDialog, Ui_TabbedModelEditor): """ Model editor "container" class describing interaction between plugin definition widget and model editor widget. Once the model is defined, it can be saved as a plugin. """ # Signals for intertab communication plugin -> editor def __init__(self, parent=None, edit_only=False): super(TabbedModelEditor, self).__init__(parent._parent) self.parent = parent self.setupUi(self) # disable the context help icon self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint) # globals self.filename = "" self.window_title = self.windowTitle() self.edit_only = edit_only self.is_modified = False self.label = None self.addWidgets() self.addSignals()
[docs] def addWidgets(self): """ Populate tabs with widgets """ # Set up widget enablement/visibility self.cmdLoad.setVisible(self.edit_only) # Add tabs # Plugin definition widget self.plugin_widget = PluginDefinition(self) self.tabWidget.addTab(self.plugin_widget, "Plugin Definition") self.setPluginActive(True) self.editor_widget = ModelEditor(self) # Initially, nothing in the editor self.editor_widget.setEnabled(False) self.tabWidget.addTab(self.editor_widget, "Model editor") self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False) if self.edit_only: self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setText("Save") # Hide signals from the plugin widget self.plugin_widget.blockSignals(True) # and hide the tab/widget itself self.tabWidget.removeTab(0)
[docs] def addSignals(self): """ Define slots for common widget signals """ # buttons self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self.onApply) self.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel).clicked.connect(self.onCancel) self.buttonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self.onHelp) self.cmdLoad.clicked.connect(self.onLoad) # signals from tabs self.plugin_widget.modelModified.connect(self.editorModelModified) self.editor_widget.modelModified.connect(self.editorModelModified) self.plugin_widget.txtName.editingFinished.connect(self.pluginTitleSet)
[docs] def setPluginActive(self, is_active=True): """ Enablement control for all the controls on the simple plugin editor """ self.plugin_widget.setEnabled(is_active)
[docs] def saveClose(self): """ Check if file needs saving before closing or model reloading """ saveCancelled = False ret = self.onModifiedExit() if ret == QtWidgets.QMessageBox.Cancel: saveCancelled = True elif ret == QtWidgets.QMessageBox.Save: self.updateFromEditor() return saveCancelled
[docs] def closeEvent(self, event): """ Overwrite the close even to assure intent """ if self.is_modified: saveCancelled = self.saveClose() if saveCancelled: return event.accept()
[docs] def onLoad(self): """ Loads a model plugin file """ if self.is_modified: saveCancelled = self.saveClose() if saveCancelled: return self.is_modified = False self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False) plugin_location = models.find_plugins_dir() filename = QtWidgets.QFileDialog.getOpenFileName( self, 'Open Plugin', plugin_location, 'SasView Plugin Model (*.py)', None, QtWidgets.QFileDialog.DontUseNativeDialog)[0] # Load the file if not filename: logging.info("No data file chosen.") return # remove c-plugin tab, if present. if self.tabWidget.count()>1: self.tabWidget.removeTab(1) self.loadFile(filename)
[docs] def loadFile(self, filename): """ Performs the load operation and updates the view """ self.editor_widget.blockSignals(True) with open(filename, 'r') as plugin: self.editor_widget.txtEditor.setPlainText(plugin.read()) self.editor_widget.setEnabled(True) self.editor_widget.blockSignals(False) self.filename = filename display_name, _ = os.path.splitext(os.path.basename(filename)) self.setWindowTitle(self.window_title + " - " + display_name) # Name the tab with .py filename display_name = os.path.basename(filename) self.tabWidget.setTabText(0, display_name) # See if there is filename.c present c_path = self.filename.replace(".py", ".c") if not os.path.isfile(c_path): return # add a tab with the same highlighting display_name = os.path.basename(c_path) self.c_editor_widget = ModelEditor(self, is_python=False) self.tabWidget.addTab(self.c_editor_widget, display_name) # Read in the file and set in on the widget with open(c_path, 'r') as plugin: self.c_editor_widget.txtEditor.setPlainText(plugin.read()) self.c_editor_widget.modelModified.connect(self.editorModelModified)
[docs] def onModifiedExit(self): msg_box = QtWidgets.QMessageBox(self) msg_box.setWindowTitle("SasView Model Editor") msg_box.setText("The document has been modified.") msg_box.setInformativeText("Do you want to save your changes?") msg_box.setStandardButtons(QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard | QtWidgets.QMessageBox.Cancel) msg_box.setDefaultButton(QtWidgets.QMessageBox.Save) return msg_box.exec()
[docs] def onCancel(self): """ Accept if document not modified, confirm intent otherwise. """ if self.is_modified: saveCancelled = self.saveClose() if saveCancelled: return self.reject()
[docs] def onApply(self): """ Write the plugin and update the model editor if plugin editor open Write/overwrite the plugin if model editor open """ if isinstance(self.tabWidget.currentWidget(), PluginDefinition): self.updateFromPlugin() else: self.updateFromEditor() self.is_modified = False
[docs] def editorModelModified(self): """ User modified the model in the Model Editor. Disable the plugin editor and show that the model is changed. """ self.setTabEdited(True) self.plugin_widget.txtFunction.setStyleSheet("") self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(True) self.is_modified = True
[docs] def pluginTitleSet(self): """ User modified the model name. Display the model name in the window title and allow for model save. """ # Ensure plugin name is non-empty model = self.getModel() if 'filename' in model and model['filename']: self.setWindowTitle(self.window_title + " - " + model['filename']) self.setTabEdited(True) self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(True) self.is_modified = True else: # the model name is empty - disable Apply and clear the editor self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False) self.editor_widget.blockSignals(True) self.editor_widget.txtEditor.setPlainText('') self.editor_widget.blockSignals(False) self.editor_widget.setEnabled(False)
[docs] def setTabEdited(self, is_edited): """ Change the widget name to indicate unsaved state Unsaved state: add "*" to filename display saved state: remove "*" from filename display """ current_text = self.windowTitle() if is_edited: if current_text[-1] != "*": current_text += "*" else: if current_text[-1] == "*": current_text = current_text[:-1] self.setWindowTitle(current_text)
[docs] def updateFromPlugin(self): """ Write the plugin and update the model editor """ # get current model model = self.getModel() if 'filename' not in model: return # get required filename filename = model['filename'] # check if file exists plugin_location = models.find_plugins_dir() full_path = os.path.join(plugin_location, filename) if os.path.splitext(full_path)[1] != ".py": full_path += ".py" # Update the global path definition self.filename = full_path if not self.canWriteModel(model, full_path): return # generate the model representation as string model_str = self.generateModel(model, full_path) self.writeFile(full_path, model_str) # disable "Apply" self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False) # test the model # Run the model test in sasmodels if not self.isModelCorrect(full_path): return self.editor_widget.setEnabled(True) # Update the editor here. # Simple string forced into control. self.editor_widget.blockSignals(True) self.editor_widget.txtEditor.setPlainText(model_str) self.editor_widget.blockSignals(False) # Set the widget title self.setTabEdited(False) # Notify listeners self.parent.communicate.customModelDirectoryChanged.emit() # Notify the user msg = "Custom model "+filename + " successfully created." self.parent.communicate.statusBarUpdateSignal.emit(msg) logging.info(msg)
[docs] def isModelCorrect(self, full_path): """ Run the sasmodels method for model check and return True if the model is good. False otherwise. """ successfulCheck = True try: model_results = GuiUtils.checkModel(full_path) logging.info(model_results) # We can't guarantee the type of the exception coming from # Sasmodels, so need the overreaching general Exception except Exception as ex: msg = "Error building model: "+ str(ex) logging.error(msg) #print three last lines of the stack trace # this will point out the exact line failing last_lines = traceback.format_exc().split('\n')[-4:] traceback_to_show = '\n'.join(last_lines) logging.error(traceback_to_show) # Set the status bar message self.parent.communicate.statusBarUpdateSignal.emit("Model check failed") # Remove the file so it is not being loaded on refresh os.remove(full_path) # Put a thick, red border around the mini-editor self.plugin_widget.txtFunction.setStyleSheet("border: 5px solid red") # Use the last line of the traceback for the tooltip last_lines = traceback.format_exc().split('\n')[-2:] traceback_to_show = '\n'.join(last_lines) self.plugin_widget.txtFunction.setToolTip(traceback_to_show) successfulCheck = False return successfulCheck
[docs] def updateFromEditor(self): """ Save the current state of the Model Editor """ filename = self.filename if not self.tabWidget.currentWidget().is_python: base, _ = os.path.splitext(filename) filename = base + '.c' # make sure we have the file handle ready assert(filename != "") # Retrieve model string model_str = self.getModel()['text'] # Save the file self.writeFile(filename, model_str) # Update the tab title self.setTabEdited(False) # Notify listeners, since the plugin name might have changed self.parent.communicate.customModelDirectoryChanged.emit() # notify the user msg = filename + " successfully saved." self.parent.communicate.statusBarUpdateSignal.emit(msg) logging.info(msg)
[docs] def canWriteModel(self, model=None, full_path=""): """ Determine if the current plugin can be written to file """ assert(isinstance(model, dict)) assert(full_path!="") # Make sure we can overwrite the file if it exists if os.path.isfile(full_path): # can we overwrite it? if not model['overwrite']: # notify the viewer msg = "Plugin with specified name already exists.\n" msg += "Please specify different filename or allow file overwrite." QtWidgets.QMessageBox.critical(self, "Plugin Error", msg) # Don't accept but return return False # Update model editor if plugin definition changed func_str = model['text'] msg = None if func_str: if 'return' not in func_str: msg = "Error: The func(x) must 'return' a value at least.\n" msg += "For example: \n\nreturn 2*x" else: msg = 'Error: Function is not defined.' if msg is not None: QtWidgets.QMessageBox.critical(self, "Plugin Error", msg) return False return True
[docs] def onHelp(self): """ Bring up the Model Editor Documentation whenever the HELP button is clicked. Calls Documentation Window with the path of the location within the documentation tree (after /doc/ ....". """ location = "/user/qtgui/Perspectives/Fitting/plugin.html" self.parent.showHelp(location)
[docs] def getModel(self): """ Retrieves plugin model from the currently open tab """ return self.tabWidget.currentWidget().getModel()
[docs] @classmethod def writeFile(cls, fname, model_str=""): """ Write model content to file "fname" """ with open(fname, 'w') as out_f: out_f.write(model_str)
[docs] def generateModel(self, model, fname): """ generate model from the current plugin state """ name = model['filename'] if not name: model['filename'] = fname name = fname desc_str = model['description'] param_str = self.strFromParamDict(model['parameters']) pd_param_str = self.strFromParamDict(model['pd_parameters']) func_str = model['text'] model_text = CUSTOM_TEMPLATE % { 'name': name, 'title': 'User model for ' + name, 'description': desc_str, 'date': datetime.datetime.now().strftime('%YYYY-%mm-%dd'), } # Write out parameters param_names = [] # to store parameter names pd_params = [] model_text += 'parameters = [ \n' model_text += '# ["name", "units", default, [lower, upper], "type", "description"],\n' if param_str: for pname, pvalue, desc in self.getParamHelper(param_str): param_names.append(pname) model_text += " ['%s', '', %s, [-inf, inf], '', '%s'],\n" % (pname, pvalue, desc) if pd_param_str: for pname, pvalue, desc in self.getParamHelper(pd_param_str): param_names.append(pname) pd_params.append(pname) model_text += " ['%s', '', %s, [-inf, inf], 'volume', '%s'],\n" % (pname, pvalue, desc) model_text += ' ]\n' # Write out function definition model_text += 'def Iq(%s):\n' % ', '.join(['x'] + param_names) model_text += ' """Absolute scattering"""\n' if "scipy." in func_str: model_text +=" import scipy\n" if "numpy." in func_str: model_text +=" import numpy\n" if "np." in func_str: model_text +=" import numpy as np\n" for func_line in func_str.split('\n'): model_text +='%s%s\n' % (" ", func_line) model_text +='## uncomment the following if Iq works for vector x\n' model_text +='#Iq.vectorized = True\n' # If polydisperse, create place holders for form_volume, ER and VR if pd_params: model_text +="\n" model_text +=CUSTOM_TEMPLATE_PD % {'args': ', '.join(pd_params)} # Create place holder for Iqxy model_text +="\n" model_text +='#def Iqxy(%s):\n' % ', '.join(["x", "y"] + param_names) model_text +='# """Absolute scattering of oriented particles."""\n' model_text +='# ...\n' model_text +='# return oriented_form(x, y, args)\n' model_text +='## uncomment the following if Iqxy works for vector x, y\n' model_text +='#Iqxy.vectorized = True\n' return model_text
[docs] @classmethod def getParamHelper(cls, param_str): """ yield a sequence of name, value pairs for the parameters in param_str Parameters can be defined by one per line by name=value, or multiple on the same line by separating the pairs by semicolon or comma. The value is optional and defaults to "1.0". """ for line in param_str.replace(';', ',').split('\n'): for item in line.split(','): defn, desc = item.split('#', 1) if '#' in item else (item, '') name, value = defn.split('=', 1) if '=' in defn else (defn, '1.0') if name: yield [v.strip() for v in (name, value, desc)]
[docs] @classmethod def strFromParamDict(cls, param_dict): """ Creates string from parameter dictionary Example:: { 0: ('variable','value'), 1: ('variable','value'), ... } """ param_str = "" for _, params in param_dict.items(): if not params[0]: continue value = 1 if params[1]: try: value = float(params[1]) except ValueError: # convert to default value = 1 param_str += params[0] + " = " + str(value) + "\n" return param_str
CUSTOM_TEMPLATE = '''\ r""" Definition ---------- Calculates %(name)s. %(description)s References ---------- Authorship and Verification --------------------------- * **Author:** --- **Date:** %(date)s * **Last Modified by:** --- **Date:** %(date)s * **Last Reviewed by:** --- **Date:** %(date)s """ from sasmodels.special import * from numpy import inf name = "%(name)s" title = "%(title)s" description = """%(description)s""" ''' CUSTOM_TEMPLATE_PD = '''\ def form_volume(%(args)s): """ Volume of the particles used to compute absolute scattering intensity and to weight polydisperse parameter contributions. """ return 0.0 def ER(%(args)s): """ Effective radius of particles to be used when computing structure factors. Input parameters are vectors ranging over the mesh of polydispersity values. """ return 0.0 def VR(%(args)s): """ Volume ratio of particles to be used when computing structure factors. Input parameters are vectors ranging over the mesh of polydispersity values. """ return 1.0 ''' SUM_TEMPLATE = """ from sasmodels.core import load_model_info from sasmodels.sasview_model import make_model_from_info model_info = load_model_info('{model1}{operator}{model2}') model_info.name = '{name}'{desc_line} Model = make_model_from_info(model_info) """ if __name__ == '__main__': app = QtWidgets.QApplication(sys.argv) sheet = TabbedModelEditor() sheet.show() app.exec_()