Source code for sas.qtgui.Utilities.AddMultEditor

"""
Widget for simple add / multiply editor.
"""
# numpy methods required for the validator! Don't remove.
# pylint: disable=unused-import,unused-wildcard-import,redefined-builtin
from numpy import *
import numpy as np

from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
import webbrowser

import os
import logging
import traceback

from sasmodels.sasview_model import load_standard_models

from sas.sascalc.fit import models

import sas.qtgui.Utilities.GuiUtils as GuiUtils

# Local UI
from sas.qtgui.Utilities.UI.AddMultEditorUI import Ui_AddMultEditorUI

# Template for the output plugin file
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}'
model_info.description = '{desc_line}'
Model = make_model_from_info(model_info)
"""

# Color of backgrounds to underline valid or invalid input
BG_WHITE = "background-color: rgb(255, 255, 255);"
BG_RED = "background-color: rgb(244, 170, 164);"


[docs]class AddMultEditor(QtWidgets.QDialog, Ui_AddMultEditorUI): """ Dialog for easy custom composite models. Provides a Dialog panel to choose two existing models (including pre-existing Plugin Models which may themselves be composite models) as well as an operation on those models (add or multiply) the resulting model will add a scale parameter and a background parameter. The user can also give a brief help for the model in the description box and must provide a unique name which is verified before the new model is saved. """
[docs] def __init__(self, parent=None): super(AddMultEditor, self).__init__(parent._parent) self.parent = parent self.setupUi(self) # disable the context help icon self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint) # uncheck self.chkOverwrite self.chkOverwrite.setChecked(False) self.canOverwriteName = False # Disabled Apply button until input of valid output plugin name self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False) # Flag for correctness of resulting name self.good_name = False self.setupSignals() self.list_models = self.readModels() self.list_standard_models = self.readModels(std_only=True) # Fill models' comboboxes self.setupModels() self.setFixedSize(self.minimumSizeHint()) # Name and directory for saving new plugin model self.plugin_filename = None self.plugin_dir = models.find_plugins_dir() # Validators rx = QtCore.QRegExp("^[A-Za-z0-9_]*$") txt_validator = QtGui.QRegExpValidator(rx) self.txtName.setValidator(txt_validator)
[docs] def setupModels(self): """ Add list of models to 'Model1' and 'Model2' comboboxes """ # Load the model dict self.cbModel1.addItems(self.list_standard_models) self.cbModel2.addItems(self.list_standard_models) # set the default initial value of Model1 and Model2 index_ini_model1 = self.cbModel1.findText('sphere', QtCore.Qt.MatchFixedString) if index_ini_model1 >= 0: self.cbModel1.setCurrentIndex(index_ini_model1) else: self.cbModel1.setCurrentIndex(0) index_ini_model2 = self.cbModel2.findText('cylinder', QtCore.Qt.MatchFixedString) if index_ini_model2 >= 0: self.cbModel2.setCurrentIndex(index_ini_model2) else: self.cbModel2.setCurrentIndex(0)
[docs] def readModels(self, std_only=False): """ Generate list of all models """ s_models = load_standard_models() models_dict = {} for model in s_models: if model.category is None: continue if std_only and 'custom' in model.category: continue models_dict[model.name] = model return sorted([model_name for model_name in models_dict])
[docs] def setupSignals(self): """ Signals from various elements """ # check existence of output filename when entering name # or when overwriting not allowed self.txtName.editingFinished.connect(self.onNameCheck) self.chkOverwrite.stateChanged.connect(self.onOverwrite) self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self.onApply) self.buttonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self.onHelp) self.buttonBox.button(QtWidgets.QDialogButtonBox.Close).clicked.connect(self.close) # change displayed equation when changing operator self.cbOperator.currentIndexChanged.connect(self.onOperatorChange)
[docs] def onOverwrite(self): """ Modify state on checkbox change """ self.canOverwriteName = self.chkOverwrite.isChecked() # State changed -> allow Apply self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(True)
[docs] def onNameCheck(self): """ Check if proposed new model name does not already exists (if the overwriting is not allowed). If not an error message not show error message is displayed """ # Get the function/file name title = self.txtName.text().lstrip().rstrip() filename = title + '.py' if self.canOverwriteName: # allow overwriting -> only valid name needs to be checked # (done with validator in __init__ above) self.good_name = True self.txtName.setStyleSheet(BG_WHITE) self.plugin_filename = os.path.join(self.plugin_dir, filename) else: # No overwriting -> check existence of filename # Create list of existing model names for comparison # fake existing regular model name list models_list = [item + '.py' for item in self.list_models] if filename in models_list: self.good_name = False self.txtName.setStyleSheet(BG_RED) msg = "Plugin with specified name already exists.\n" msg += "Please specify different filename or allow file overwrite." logging.warning(msg) QtWidgets.QMessageBox.critical(self, 'Plugin Error', msg) else: s_title = title if len(title) > 20: s_title = title[0:19] + '...' logging.info("Model function ({}) has been set!\n". format(str(s_title))) self.good_name = True self.txtName.setStyleSheet(BG_WHITE) self.plugin_filename = os.path.join(self.plugin_dir, filename) # Enable Apply push button only if valid name self.buttonBox.button( QtWidgets.QDialogButtonBox.Apply).setEnabled(self.good_name)
[docs] def onOperatorChange(self, index): """ Respond to operator combo box changes """ self.lblEquation.setText('<html><head/><body><p><span style=" font-weight:600;">' 'Plugin_model = scale_factor * ' '(model_1 {} model_2) + background</span></p><p>' '<p>To add/multiply plugin models, or combine more than two models, ' 'please check Help below.<br/></p></body></html>'. format(self.cbOperator.currentText()))
[docs] def onApply(self): """ Validity check, save model to file """ # Set the button enablement, so no double clicks can be made self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False) # Check the name/overwrite combination again, in case we managed to confuse the UI self.onNameCheck() if not self.good_name: return self.write_new_model_to_file(self.plugin_filename, self.cbModel1.currentText(), self.cbModel2.currentText(), self.cbOperator.currentText()) try: success = GuiUtils.checkModel(self.plugin_filename) except Exception as ex: # broad exception from sasmodels 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") return if not success: return # Update list of models in FittingWidget and AddMultEditor self.parent.communicate.customModelDirectoryChanged.emit() # Re-read the model list so the new model is included self.list_models = self.readModels() self.updateModels() # Notify the user title = self.txtName.text().lstrip().rstrip() msg = "Custom model "+title + " successfully created." self.parent.communicate.statusBarUpdateSignal.emit(msg) logging.info(msg)
[docs] def write_new_model_to_file(self, fname, model1_name, model2_name, operator): """ Write and Save file """ description = self.txtDescription.text().lstrip().rstrip() if description.strip() != '': # Sasmodels generates a description for us. If the user provides # their own description, add a line to overwrite the sasmodels one desc_line = description else: desc_line = "{} {} {}".format(model1_name, operator, model2_name) name = os.path.splitext(os.path.basename(fname))[0] output = SUM_TEMPLATE.format(name=name, model1=model1_name, model2=model2_name, operator=operator, desc_line=desc_line) with open(fname, 'w') as out_f: out_f.write(output)
[docs] def updateModels(self): """ Update contents of comboboxes with new plugin models """ # Keep pointers to the current indices so we can show the comboboxes with # original selection model_1 = self.cbModel1.currentText() model_2 = self.cbModel2.currentText() self.cbModel1.blockSignals(True) self.cbModel1.clear() self.cbModel1.blockSignals(False) self.cbModel2.blockSignals(True) self.cbModel2.clear() self.cbModel2.blockSignals(False) # Retrieve the list of models model_list = self.readModels(std_only=True) # Populate the models comboboxes self.cbModel1.addItems(model_list) self.cbModel2.addItems(model_list) # Scroll back to the user chosen models self.cbModel1.setCurrentIndex(self.cbModel1.findText(model_1)) self.cbModel2.setCurrentIndex(self.cbModel2.findText(model_2))
[docs] def onHelp(self): """ Display related help section """ try: help_location = GuiUtils.HELP_DIRECTORY_LOCATION + \ "/user/qtgui/Perspectives/Fitting/fitting_help.html#add-multiply-models" webbrowser.open('file://' + os.path.realpath(help_location)) except AttributeError: # No manager defined - testing and standalone runs pass