source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 9c05f22

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 9c05f22 was 9c05f22, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Allow model-specific help be shown when a model is defined.

  • Property mode set to 100644
File size: 160.4 KB
RevLine 
[60af928]1import json
[cd31251]2import os
[9c05f22]3import sys
[60af928]4from collections import defaultdict
[b3e8629]5
[d4dac80]6import copy
[5236449]7import logging
8import traceback
[cbcdd2c]9from twisted.internet import threads
[1bc27f1]10import numpy as np
[e90988c]11import webbrowser
[5236449]12
[4992ff2]13from PyQt5 import QtCore
14from PyQt5 import QtGui
15from PyQt5 import QtWidgets
[60af928]16
17from sasmodels import generate
18from sasmodels import modelinfo
[5236449]19from sasmodels.sasview_model import load_standard_models
[18d5c94a]20from sasmodels.sasview_model import MultiplicationModel
[358b39d]21from sasmodels.weights import MODELS as POLYDISPERSITY_MODELS
22
[f182f93]23from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
[57be490]24from sas.sascalc.fit.pagestate import PageState
[5236449]25
[83eb5208]26import sas.qtgui.Utilities.GuiUtils as GuiUtils
[14ec91c5]27import sas.qtgui.Utilities.LocalConfig as LocalConfig
[dc5ef15]28from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
29from sas.qtgui.Plotting.PlotterData import Data1D
30from sas.qtgui.Plotting.PlotterData import Data2D
[86d3207]31from sas.qtgui.Plotting.Plotter import PlotterWidget
[5236449]32
[1bc27f1]33from sas.qtgui.Perspectives.Fitting.UI.FittingWidgetUI import Ui_FittingWidgetUI
[dc5ef15]34from sas.qtgui.Perspectives.Fitting.FitThread import FitThread
[7adc2a8]35from sas.qtgui.Perspectives.Fitting.ConsoleUpdate import ConsoleUpdate
36
[dc5ef15]37from sas.qtgui.Perspectives.Fitting.ModelThread import Calc1D
38from sas.qtgui.Perspectives.Fitting.ModelThread import Calc2D
[4d457df]39from sas.qtgui.Perspectives.Fitting.FittingLogic import FittingLogic
40from sas.qtgui.Perspectives.Fitting import FittingUtilities
[3b3b40b]41from sas.qtgui.Perspectives.Fitting import ModelUtilities
[1bc27f1]42from sas.qtgui.Perspectives.Fitting.SmearingWidget import SmearingWidget
43from sas.qtgui.Perspectives.Fitting.OptionsWidget import OptionsWidget
44from sas.qtgui.Perspectives.Fitting.FitPage import FitPage
[ad6b4e2]45from sas.qtgui.Perspectives.Fitting.ViewDelegate import ModelViewDelegate
[6011788]46from sas.qtgui.Perspectives.Fitting.ViewDelegate import PolyViewDelegate
[b00414d]47from sas.qtgui.Perspectives.Fitting.ViewDelegate import MagnetismViewDelegate
[14ec91c5]48from sas.qtgui.Perspectives.Fitting.Constraint import Constraint
[eae226b]49from sas.qtgui.Perspectives.Fitting.MultiConstraint import MultiConstraint
[57be490]50from sas.qtgui.Perspectives.Fitting.ReportPageLogic import ReportPageLogic
51
[60af928]52TAB_MAGNETISM = 4
53TAB_POLY = 3
[cbcdd2c]54CATEGORY_DEFAULT = "Choose category..."
[2eeda93]55MODEL_DEFAULT = "Choose model..."
[4d457df]56CATEGORY_STRUCTURE = "Structure Factor"
[3b3b40b]57CATEGORY_CUSTOM = "Plugin Models"
[351b53e]58STRUCTURE_DEFAULT = "None"
[60af928]59
[358b39d]60DEFAULT_POLYDISP_FUNCTION = 'gaussian'
61
[aa82f54]62# CRUFT: remove when new release of sasmodels is available
63# https://github.com/SasView/sasview/pull/181#discussion_r218135162
64from sasmodels.sasview_model import SasviewModel
65if not hasattr(SasviewModel, 'get_weights'):
66    def get_weights(self, name):
[a8b3e2c]67        """
68        Returns the polydispersity distribution for parameter *name* as *value* and *weight* arrays.
69        """
70        # type: (str) -> Tuple(np.ndarray, np.ndarray)
71        _, x, w = self._get_weights(self._model_info.parameters[name])
72        return x, w
[aa82f54]73
74    SasviewModel.get_weights = get_weights
[7adc2a8]75
[dc71408]76logger = logging.getLogger(__name__)
77
[13cd397]78class ToolTippedItemModel(QtGui.QStandardItemModel):
[f54ce30]79    """
80    Subclass from QStandardItemModel to allow displaying tooltips in
81    QTableView model.
82    """
[d0dfcb2]83    def __init__(self, parent=None):
[8e2cd79]84        QtGui.QStandardItemModel.__init__(self, parent)
[13cd397]85
[fbfc488]86    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
[f54ce30]87        """
88        Displays tooltip for each column's header
89        :param section:
90        :param orientation:
91        :param role:
92        :return:
93        """
[13cd397]94        if role == QtCore.Qt.ToolTipRole:
95            if orientation == QtCore.Qt.Horizontal:
[b3e8629]96                return str(self.header_tooltips[section])
[ca7c6bd]97
[a95c44b]98        return QtGui.QStandardItemModel.headerData(self, section, orientation, role)
[13cd397]99
[4992ff2]100class FittingWidget(QtWidgets.QWidget, Ui_FittingWidgetUI):
[60af928]101    """
[f46f6dc]102    Main widget for selecting form and structure factor models
[60af928]103    """
[be8f4b0]104    constraintAddedSignal = QtCore.pyqtSignal(list)
105    newModelSignal = QtCore.pyqtSignal()
[3b3b40b]106    fittingFinishedSignal = QtCore.pyqtSignal(tuple)
107    batchFittingFinishedSignal = QtCore.pyqtSignal(tuple)
[dcabba7]108    Calc1DFinishedSignal = QtCore.pyqtSignal(dict)
109    Calc2DFinishedSignal = QtCore.pyqtSignal(dict)
[3b3b40b]110
[672c0e97]111    MAGNETIC_MODELS = ['sphere', 'core_shell_sphere', 'core_multi_shell', 'cylinder', 'parallelepiped']
112
[1bc27f1]113    def __init__(self, parent=None, data=None, tab_id=1):
[60af928]114
115        super(FittingWidget, self).__init__()
116
[86f88d1]117        # Necessary globals
[cbcdd2c]118        self.parent = parent
[2a432e7]119
120        # Which tab is this widget displayed in?
121        self.tab_id = tab_id
122
[d2007a8]123        import sys
124        sys.excepthook = self.info
125
[2a432e7]126        # Globals
127        self.initializeGlobals()
128
[d4dac80]129        # data index for the batch set
130        self.data_index = 0
131        # Main Data[12]D holders
132        # Logics.data contains a single Data1D/Data2D object
133        self._logic = [FittingLogic()]
134
[2a432e7]135        # Main GUI setup up
136        self.setupUi(self)
137        self.setWindowTitle("Fitting")
138
139        # Set up tabs widgets
140        self.initializeWidgets()
141
142        # Set up models and views
143        self.initializeModels()
144
145        # Defaults for the structure factors
146        self.setDefaultStructureCombo()
147
148        # Make structure factor and model CBs disabled
149        self.disableModelCombo()
150        self.disableStructureCombo()
151
152        # Generate the category list for display
153        self.initializeCategoryCombo()
154
155        # Initial control state
156        self.initializeControls()
157
[ebfe223]158        QtWidgets.QApplication.processEvents()
159
160        # Connect signals to controls
161        self.initializeSignals()
162
[d4dac80]163        if data is not None:
164            self.data = data
[2a432e7]165
[457d961]166        # New font to display angstrom symbol
167        new_font = 'font-family: -apple-system, "Helvetica Neue", "Ubuntu";'
168        self.label_17.setStyleSheet(new_font)
169        self.label_19.setStyleSheet(new_font)
170
[d2007a8]171    def info(self, type, value, tb):
172        logger.error("SasView threw exception: " + str(value))
173        traceback.print_exception(type, value, tb)
174
[d4dac80]175    @property
176    def logic(self):
177        # make sure the logic contains at least one element
[8e2cd79]178        assert self._logic
[d4dac80]179        # logic connected to the currently shown data
180        return self._logic[self.data_index]
[2a432e7]181
182    @property
183    def data(self):
184        return self.logic.data
185
186    @data.setter
187    def data(self, value):
188        """ data setter """
[f7d14a1]189        # Value is either a list of indices for batch fitting or a simple index
190        # for standard fitting. Assure we have a list, regardless.
[ee18d33]191        if isinstance(value, list):
192            self.is_batch_fitting = True
193        else:
194            value = [value]
195
196        assert isinstance(value[0], QtGui.QStandardItem)
197
198        # Keep reference to all datasets for batch
199        self.all_data = value
[2a432e7]200
[d4dac80]201        # Create logics with data items
[f7d14a1]202        # Logics.data contains only a single Data1D/Data2D object
[87dfca4]203        if len(value) == 1:
204            # single data logic is already defined, update data on it
205            self._logic[0].data = GuiUtils.dataFromItem(value[0])
206        else:
207            # batch datasets
[54492dc]208            self._logic = []
[87dfca4]209            for data_item in value:
210                logic = FittingLogic(data=GuiUtils.dataFromItem(data_item))
211                self._logic.append(logic)
[2a432e7]212
213        # Overwrite data type descriptor
[339e22b]214
[2a432e7]215        self.is2D = True if isinstance(self.logic.data, Data2D) else False
216
[f7d14a1]217        # Let others know we're full of data now
[2a432e7]218        self.data_is_loaded = True
219
220        # Enable/disable UI components
221        self.setEnablementOnDataLoad()
222
223    def initializeGlobals(self):
224        """
225        Initialize global variables used in this class
226        """
[cbcdd2c]227        # SasModel is loaded
[60af928]228        self.model_is_loaded = False
[cbcdd2c]229        # Data[12]D passed and set
[5236449]230        self.data_is_loaded = False
[ee18d33]231        # Batch/single fitting
232        self.is_batch_fitting = False
[7fd20fc]233        self.is_chain_fitting = False
[ded5e77]234        # Is the fit job running?
[8e2cd79]235        self.fit_started = False
[ded5e77]236        # The current fit thread
237        self.calc_fit = None
[cbcdd2c]238        # Current SasModel in view
[5236449]239        self.kernel_module = None
[cbcdd2c]240        # Current SasModel view dimension
[60af928]241        self.is2D = False
[cbcdd2c]242        # Current SasModel is multishell
[86f88d1]243        self.model_has_shells = False
[cbcdd2c]244        # Utility variable to enable unselectable option in category combobox
[86f88d1]245        self._previous_category_index = 0
[70f4458]246        # Utility variables for multishell display
247        self._n_shells_row = 0
248        self._num_shell_params = 0
[cbcdd2c]249        # Dictionary of {model name: model class} for the current category
[5236449]250        self.models = {}
[f182f93]251        # Parameters to fit
[6dbff18]252        self.main_params_to_fit = []
253        self.poly_params_to_fit = []
254        self.magnet_params_to_fit = []
255
[180bd54]256        # Fit options
257        self.q_range_min = 0.005
258        self.q_range_max = 0.1
259        self.npts = 25
260        self.log_points = False
261        self.weighting = 0
[2add354]262        self.chi2 = None
[6011788]263        # Does the control support UNDO/REDO
264        # temporarily off
[2241130]265        self.undo_supported = False
266        self.page_stack = []
[377ade1]267        self.all_data = []
[3b3b40b]268        # custom plugin models
269        # {model.name:model}
270        self.custom_models = self.customModels()
[8222f171]271        # Polydisp widget table default index for function combobox
272        self.orig_poly_index = 3
[d4dac80]273        # copy of current kernel model
274        self.kernel_module_copy = None
[6011788]275
[66d4370]276        # dictionaries of current params
277        self.poly_params = {}
278        self.magnet_params = {}
279
[116dd4c1]280        # Page id for fitting
281        # To keep with previous SasView values, use 200 as the start offset
282        self.page_id = 200 + self.tab_id
283
[d48cc19]284        # Data for chosen model
285        self.model_data = None
286
[a9b568c]287        # Which shell is being currently displayed?
288        self.current_shell_displayed = 0
[0d13814]289        # List of all shell-unique parameters
290        self.shell_names = []
[b00414d]291
292        # Error column presence in parameter display
[f182f93]293        self.has_error_column = False
[aca8418]294        self.has_poly_error_column = False
[b00414d]295        self.has_magnet_error_column = False
[a9b568c]296
[7fb68060]297        # Enablement of comboboxes
298        self.enabled_cbmodel = False
299        self.enabled_sfmodel = False
300
[cb90b65]301        # If the widget generated theory item, save it
302        self.theory_item = None
303
[f37cab0]304        # list column widths
305        self.lstParamHeaderSizes = {}
306
[2a432e7]307        # signal communicator
[cbcdd2c]308        self.communicate = self.parent.communicate
[60af928]309
[2a432e7]310    def initializeWidgets(self):
311        """
312        Initialize widgets for tabs
313        """
[180bd54]314        # Options widget
[4992ff2]315        layout = QtWidgets.QGridLayout()
[180bd54]316        self.options_widget = OptionsWidget(self, self.logic)
[1bc27f1]317        layout.addWidget(self.options_widget)
[180bd54]318        self.tabOptions.setLayout(layout)
319
[e1e3e09]320        # Smearing widget
[4992ff2]321        layout = QtWidgets.QGridLayout()
[e1e3e09]322        self.smearing_widget = SmearingWidget(self)
[1bc27f1]323        layout.addWidget(self.smearing_widget)
[180bd54]324        self.tabResolution.setLayout(layout)
[e1e3e09]325
[b1e36a3]326        # Define bold font for use in various controls
[1bc27f1]327        self.boldFont = QtGui.QFont()
[a0f5c36]328        self.boldFont.setBold(True)
329
330        # Set data label
[b1e36a3]331        self.label.setFont(self.boldFont)
332        self.label.setText("No data loaded")
333        self.lblFilename.setText("")
334
[6ff2eb3]335        # Magnetic angles explained in one picture
[4992ff2]336        self.magneticAnglesWidget = QtWidgets.QWidget()
337        labl = QtWidgets.QLabel(self.magneticAnglesWidget)
[6d87082]338        pixmap = QtGui.QPixmap(GuiUtils.IMAGES_DIRECTORY_LOCATION + '/M_angles_pic.png')
[6ff2eb3]339        labl.setPixmap(pixmap)
340        self.magneticAnglesWidget.setFixedSize(pixmap.width(), pixmap.height())
341
[2a432e7]342    def initializeModels(self):
343        """
344        Set up models and views
345        """
[86f88d1]346        # Set the main models
[cd31251]347        # We can't use a single model here, due to restrictions on flattening
348        # the model tree with subclassed QAbstractProxyModel...
[13cd397]349        self._model_model = ToolTippedItemModel()
350        self._poly_model = ToolTippedItemModel()
351        self._magnet_model = ToolTippedItemModel()
[60af928]352
353        # Param model displayed in param list
354        self.lstParams.setModel(self._model_model)
[5236449]355        self.readCategoryInfo()
[4992ff2]356
[60af928]357        self.model_parameters = None
[ad6b4e2]358
359        # Delegates for custom editing and display
360        self.lstParams.setItemDelegate(ModelViewDelegate(self))
361
[86f88d1]362        self.lstParams.setAlternatingRowColors(True)
[61a92d4]363        stylesheet = """
[457d961]364
365            QTreeView {
366                paint-alternating-row-colors-for-empty-area:0;
367            }
368
[97df8a9]369            QTreeView::item {
370                border: 1px;
[ebfe223]371                padding: 2px 1px;
[97df8a9]372            }
373
[2a432e7]374            QTreeView::item:hover {
375                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1);
376                border: 1px solid #bfcde4;
[61a92d4]377            }
[2a432e7]378
379            QTreeView::item:selected {
380                border: 1px solid #567dbc;
381            }
382
383            QTreeView::item:selected:active{
384                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6ea1f1, stop: 1 #567dbc);
385            }
386
387            QTreeView::item:selected:!active {
388                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6b9be8, stop: 1 #577fbf);
389            }
390           """
[61a92d4]391        self.lstParams.setStyleSheet(stylesheet)
[672b8ab]392        self.lstParams.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
[7fd20fc]393        self.lstParams.customContextMenuRequested.connect(self.showModelContextMenu)
[457d961]394        self.lstParams.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False)
[f37cab0]395        # Column resize signals
396        self.lstParams.header().sectionResized.connect(self.onColumnWidthUpdate)
397
[60af928]398        # Poly model displayed in poly list
[811bec1]399        self.lstPoly.setModel(self._poly_model)
[60af928]400        self.setPolyModel()
401        self.setTableProperties(self.lstPoly)
[6011788]402        # Delegates for custom editing and display
[aca8418]403        self.lstPoly.setItemDelegate(PolyViewDelegate(self))
404        # Polydispersity function combo response
405        self.lstPoly.itemDelegate().combo_updated.connect(self.onPolyComboIndexChange)
[e43fc91]406        self.lstPoly.itemDelegate().filename_updated.connect(self.onPolyFilenameChange)
[60af928]407
408        # Magnetism model displayed in magnetism list
409        self.lstMagnetic.setModel(self._magnet_model)
410        self.setMagneticModel()
411        self.setTableProperties(self.lstMagnetic)
[b00414d]412        # Delegates for custom editing and display
413        self.lstMagnetic.setItemDelegate(MagnetismViewDelegate(self))
[60af928]414
[2a432e7]415    def initializeCategoryCombo(self):
416        """
417        Model category combo setup
418        """
[60af928]419        category_list = sorted(self.master_category_dict.keys())
[86f88d1]420        self.cbCategory.addItem(CATEGORY_DEFAULT)
[60af928]421        self.cbCategory.addItems(category_list)
[3d18691]422        if CATEGORY_STRUCTURE not in category_list:
423            self.cbCategory.addItem(CATEGORY_STRUCTURE)
[6f7f652]424        self.cbCategory.setCurrentIndex(0)
[60af928]425
[e1e3e09]426    def setEnablementOnDataLoad(self):
427        """
428        Enable/disable various UI elements based on data loaded
429        """
[cbcdd2c]430        # Tag along functionality
[b1e36a3]431        self.label.setText("Data loaded from: ")
[6b50296]432        if self.logic.data.filename:
433            self.lblFilename.setText(self.logic.data.filename)
434        else:
435            self.lblFilename.setText(self.logic.data.name)
[5236449]436        self.updateQRange()
[e1e3e09]437        # Switch off Data2D control
438        self.chk2DView.setEnabled(False)
439        self.chk2DView.setVisible(False)
[672c0e97]440        self.chkMagnetism.setEnabled(False)
[e20870bc]441        self.tabFitting.setTabEnabled(TAB_MAGNETISM, self.chkMagnetism.isChecked())
[ee18d33]442        # Combo box or label for file name"
443        if self.is_batch_fitting:
444            self.lblFilename.setVisible(False)
445            for dataitem in self.all_data:
446                filename = GuiUtils.dataFromItem(dataitem).filename
447                self.cbFileNames.addItem(filename)
448            self.cbFileNames.setVisible(True)
[7fd20fc]449            self.chkChainFit.setEnabled(True)
450            self.chkChainFit.setVisible(True)
[38eb433]451            # This panel is not designed to view individual fits, so disable plotting
452            self.cmdPlot.setVisible(False)
[180bd54]453        # Similarly on other tabs
454        self.options_widget.setEnablementOnDataLoad()
[f7d14a1]455        self.onSelectModel()
[e1e3e09]456        # Smearing tab
[9a7c81c]457        self.smearing_widget.updateData(self.data)
[60af928]458
[f46f6dc]459    def acceptsData(self):
460        """ Tells the caller this widget can accept new dataset """
[5236449]461        return not self.data_is_loaded
[f46f6dc]462
[6f7f652]463    def disableModelCombo(self):
[cbcdd2c]464        """ Disable the combobox """
[6f7f652]465        self.cbModel.setEnabled(False)
[b1e36a3]466        self.lblModel.setEnabled(False)
[7fb68060]467        self.enabled_cbmodel = False
[6f7f652]468
469    def enableModelCombo(self):
[cbcdd2c]470        """ Enable the combobox """
[6f7f652]471        self.cbModel.setEnabled(True)
[b1e36a3]472        self.lblModel.setEnabled(True)
[7fb68060]473        self.enabled_cbmodel = True
[6f7f652]474
475    def disableStructureCombo(self):
[cbcdd2c]476        """ Disable the combobox """
[6f7f652]477        self.cbStructureFactor.setEnabled(False)
[b1e36a3]478        self.lblStructure.setEnabled(False)
[7fb68060]479        self.enabled_sfmodel = False
[6f7f652]480
481    def enableStructureCombo(self):
[cbcdd2c]482        """ Enable the combobox """
[6f7f652]483        self.cbStructureFactor.setEnabled(True)
[b1e36a3]484        self.lblStructure.setEnabled(True)
[7fb68060]485        self.enabled_sfmodel = True
[6f7f652]486
[0268aed]487    def togglePoly(self, isChecked):
[454670d]488        """ Enable/disable the polydispersity tab """
[0268aed]489        self.tabFitting.setTabEnabled(TAB_POLY, isChecked)
[d4216ac]490        # Check if any parameters are ready for fitting
491        self.cmdFit.setEnabled(self.haveParamsToFit())
[0268aed]492
493    def toggleMagnetism(self, isChecked):
[454670d]494        """ Enable/disable the magnetism tab """
[0268aed]495        self.tabFitting.setTabEnabled(TAB_MAGNETISM, isChecked)
[d4216ac]496        # Check if any parameters are ready for fitting
497        self.cmdFit.setEnabled(self.haveParamsToFit())
[0268aed]498
[7fd20fc]499    def toggleChainFit(self, isChecked):
500        """ Enable/disable chain fitting """
501        self.is_chain_fitting = isChecked
502
[0268aed]503    def toggle2D(self, isChecked):
[454670d]504        """ Enable/disable the controls dependent on 1D/2D data instance """
[0268aed]505        self.chkMagnetism.setEnabled(isChecked)
506        self.is2D = isChecked
[1970780]507        # Reload the current model
[e1e3e09]508        if self.kernel_module:
509            self.onSelectModel()
[5236449]510
[8b480d27]511    @classmethod
512    def customModels(cls):
[3b3b40b]513        """ Reads in file names in the custom plugin directory """
514        return ModelUtilities._find_models()
515
[86f88d1]516    def initializeControls(self):
517        """
518        Set initial control enablement
519        """
[ee18d33]520        self.cbFileNames.setVisible(False)
[86f88d1]521        self.cmdFit.setEnabled(False)
[d48cc19]522        self.cmdPlot.setEnabled(False)
[180bd54]523        self.options_widget.cmdComputePoints.setVisible(False) # probably redundant
[86f88d1]524        self.chkPolydispersity.setEnabled(True)
525        self.chkPolydispersity.setCheckState(False)
526        self.chk2DView.setEnabled(True)
527        self.chk2DView.setCheckState(False)
528        self.chkMagnetism.setEnabled(False)
529        self.chkMagnetism.setCheckState(False)
[7fd20fc]530        self.chkChainFit.setEnabled(False)
531        self.chkChainFit.setVisible(False)
[cbcdd2c]532        # Tabs
[86f88d1]533        self.tabFitting.setTabEnabled(TAB_POLY, False)
534        self.tabFitting.setTabEnabled(TAB_MAGNETISM, False)
535        self.lblChi2Value.setText("---")
[e1e3e09]536        # Smearing tab
[9a7c81c]537        self.smearing_widget.updateData(self.data)
[180bd54]538        # Line edits in the option tab
539        self.updateQRange()
[86f88d1]540
541    def initializeSignals(self):
542        """
543        Connect GUI element signals
544        """
[cbcdd2c]545        # Comboboxes
[cd31251]546        self.cbStructureFactor.currentIndexChanged.connect(self.onSelectStructureFactor)
547        self.cbCategory.currentIndexChanged.connect(self.onSelectCategory)
548        self.cbModel.currentIndexChanged.connect(self.onSelectModel)
[ee18d33]549        self.cbFileNames.currentIndexChanged.connect(self.onSelectBatchFilename)
[cbcdd2c]550        # Checkboxes
[86f88d1]551        self.chk2DView.toggled.connect(self.toggle2D)
552        self.chkPolydispersity.toggled.connect(self.togglePoly)
553        self.chkMagnetism.toggled.connect(self.toggleMagnetism)
[7fd20fc]554        self.chkChainFit.toggled.connect(self.toggleChainFit)
[cbcdd2c]555        # Buttons
[5236449]556        self.cmdFit.clicked.connect(self.onFit)
[cbcdd2c]557        self.cmdPlot.clicked.connect(self.onPlot)
[2add354]558        self.cmdHelp.clicked.connect(self.onHelp)
[6ff2eb3]559        self.cmdMagneticDisplay.clicked.connect(self.onDisplayMagneticAngles)
[cbcdd2c]560
561        # Respond to change in parameters from the UI
[d2007a8]562        self._model_model.dataChanged.connect(self.onMainParamsChange)
563        self._poly_model.dataChanged.connect(self.onPolyModelChange)
564        self._magnet_model.dataChanged.connect(self.onMagnetModelChange)
[3b3b40b]565        self.lstParams.selectionModel().selectionChanged.connect(self.onSelectionChanged)
566
567        # Local signals
568        self.batchFittingFinishedSignal.connect(self.batchFitComplete)
569        self.fittingFinishedSignal.connect(self.fitComplete)
[d4dac80]570        self.Calc1DFinishedSignal.connect(self.complete1D)
571        self.Calc2DFinishedSignal.connect(self.complete2D)
[86f88d1]572
[180bd54]573        # Signals from separate tabs asking for replot
574        self.options_widget.plot_signal.connect(self.onOptionsUpdate)
575
[3b3b40b]576        # Signals from other widgets
577        self.communicate.customModelDirectoryChanged.connect(self.onCustomModelChange)
[9a7c81c]578        self.smearing_widget.smearingChangedSignal.connect(self.onSmearingOptionsUpdate)
[3b3b40b]579
[3d18691]580        # Communicator signal
581        self.communicate.updateModelCategoriesSignal.connect(self.onCategoriesChanged)
582
[d3c0b95]583    def modelName(self):
584        """
585        Returns model name, by default M<tab#>, e.g. M1, M2
586        """
587        return "M%i" % self.tab_id
588
589    def nameForFittedData(self, name):
590        """
591        Generate name for the current fit
592        """
593        if self.is2D:
594            name += "2d"
595        name = "%s [%s]" % (self.modelName(), name)
596        return name
[7fd20fc]597
[d3c0b95]598    def showModelContextMenu(self, position):
599        """
600        Show context specific menu in the parameter table.
601        When clicked on parameter(s): fitting/constraints options
602        When clicked on white space: model description
603        """
[50cafe7]604        rows = [s.row() for s in self.lstParams.selectionModel().selectedRows()
605                if self.isCheckable(s.row())]
[7fd20fc]606        menu = self.showModelDescription() if not rows else self.modelContextMenu(rows)
607        try:
608            menu.exec_(self.lstParams.viewport().mapToGlobal(position))
609        except AttributeError as ex:
[bb477f5]610            logger.error("Error generating context menu: %s" % ex)
[7fd20fc]611        return
612
613    def modelContextMenu(self, rows):
[eae226b]614        """
[d3c0b95]615        Create context menu for the parameter selection
[eae226b]616        """
[7fd20fc]617        menu = QtWidgets.QMenu()
[eae226b]618        num_rows = len(rows)
[63319b0]619        if num_rows < 1:
620            return menu
[7fd20fc]621        # Select for fitting
[8e2cd79]622        param_string = "parameter " if num_rows == 1 else "parameters "
623        to_string = "to its current value" if num_rows == 1 else "to their current values"
[d3c0b95]624        has_constraints = any([self.rowHasConstraint(i) for i in rows])
[baeac95]625        has_real_constraints = any([self.rowHasActiveConstraint(i) for i in rows])
[eae226b]626
[7fd20fc]627        self.actionSelect = QtWidgets.QAction(self)
628        self.actionSelect.setObjectName("actionSelect")
[eae226b]629        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
[7fd20fc]630        # Unselect from fitting
631        self.actionDeselect = QtWidgets.QAction(self)
632        self.actionDeselect.setObjectName("actionDeselect")
[eae226b]633        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
[7fd20fc]634
635        self.actionConstrain = QtWidgets.QAction(self)
636        self.actionConstrain.setObjectName("actionConstrain")
[eae226b]637        self.actionConstrain.setText(QtCore.QCoreApplication.translate("self", "Constrain "+param_string + to_string))
638
639        self.actionRemoveConstraint = QtWidgets.QAction(self)
640        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
641        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove constraint"))
[7fd20fc]642
[baeac95]643        self.actionEditConstraint = QtWidgets.QAction(self)
644        self.actionEditConstraint.setObjectName("actionEditConstrain")
645        self.actionEditConstraint.setText(QtCore.QCoreApplication.translate("self", "Edit constraint"))
646
[7fd20fc]647        self.actionMultiConstrain = QtWidgets.QAction(self)
648        self.actionMultiConstrain.setObjectName("actionMultiConstrain")
649        self.actionMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Constrain selected parameters to their current values"))
650
651        self.actionMutualMultiConstrain = QtWidgets.QAction(self)
652        self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
653        self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of selected parameters..."))
654
655        menu.addAction(self.actionSelect)
656        menu.addAction(self.actionDeselect)
657        menu.addSeparator()
658
[d3c0b95]659        if has_constraints:
[eae226b]660            menu.addAction(self.actionRemoveConstraint)
[baeac95]661            if num_rows == 1 and has_real_constraints:
662                menu.addAction(self.actionEditConstraint)
[d3c0b95]663            #if num_rows == 1:
664            #    menu.addAction(self.actionEditConstraint)
[eae226b]665        else:
[7fd20fc]666            menu.addAction(self.actionConstrain)
[d3c0b95]667            if num_rows == 2:
668                menu.addAction(self.actionMutualMultiConstrain)
[7fd20fc]669
670        # Define the callbacks
[0595bb7]671        self.actionConstrain.triggered.connect(self.addSimpleConstraint)
[eae226b]672        self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
[baeac95]673        self.actionEditConstraint.triggered.connect(self.editConstraint)
[0595bb7]674        self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
[7fd20fc]675        self.actionSelect.triggered.connect(self.selectParameters)
676        self.actionDeselect.triggered.connect(self.deselectParameters)
677        return menu
678
[0595bb7]679    def showMultiConstraint(self):
[7fd20fc]680        """
681        Show the constraint widget and receive the expression
682        """
[0595bb7]683        selected_rows = self.lstParams.selectionModel().selectedRows()
[6e58f2f]684        # There have to be only two rows selected. The caller takes care of that
685        # but let's check the correctness.
[8e2cd79]686        assert len(selected_rows) == 2
[0595bb7]687
688        params_list = [s.data() for s in selected_rows]
[eae226b]689        # Create and display the widget for param1 and param2
[7fd20fc]690        mc_widget = MultiConstraint(self, params=params_list)
[305114c]691        # Check if any of the parameters are polydisperse
692        if not np.any([FittingUtilities.isParamPolydisperse(p, self.model_parameters, is2D=self.is2D) for p in params_list]):
693            # no parameters are pd - reset the text to not show the warning
694            mc_widget.lblWarning.setText("")
[eae226b]695        if mc_widget.exec_() != QtWidgets.QDialog.Accepted:
696            return
697
[0595bb7]698        constraint = Constraint()
699        c_text = mc_widget.txtConstraint.text()
700
[eae226b]701        # widget.params[0] is the parameter we're constraining
702        constraint.param = mc_widget.params[0]
[3b3b40b]703        # parameter should have the model name preamble
[06234fc]704        model_name = self.kernel_module.name
[3b3b40b]705        # param_used is the parameter we're using in constraining function
706        param_used = mc_widget.params[1]
707        # Replace param_used with model_name.param_used
708        updated_param_used = model_name + "." + param_used
709        new_func = c_text.replace(param_used, updated_param_used)
710        constraint.func = new_func
[baeac95]711        constraint.value_ex = updated_param_used
[d3c0b95]712        # Which row is the constrained parameter in?
[2d466e4]713        row = self.getRowFromName(constraint.param)
[eae226b]714
[baeac95]715        # what is the parameter to constraint to?
716        constraint.value = param_used
717
[09e0c32]718        # Should the new constraint be validated?
719        constraint.validate = mc_widget.validate
720
[6e58f2f]721        # Create a new item and add the Constraint object as a child
722        self.addConstraintToRow(constraint=constraint, row=row)
[eae226b]723
[2d466e4]724    def getRowFromName(self, name):
[eae226b]725        """
[d3c0b95]726        Given parameter name get the row number in self._model_model
[eae226b]727        """
728        for row in range(self._model_model.rowCount()):
729            row_name = self._model_model.item(row).text()
730            if row_name == name:
731                return row
732        return None
733
[2d466e4]734    def getParamNames(self):
735        """
736        Return list of all parameters for the current model
737        """
[00b7ddf0]738        return [self._model_model.item(row).text()
739                for row in range(self._model_model.rowCount())
740                if self.isCheckable(row)]
[2d466e4]741
[eae226b]742    def modifyViewOnRow(self, row, font=None, brush=None):
743        """
744        Chage how the given row of the main model is shown
745        """
746        fields_enabled = False
747        if font is None:
748            font = QtGui.QFont()
749            fields_enabled = True
750        if brush is None:
751            brush = QtGui.QBrush()
752            fields_enabled = True
[0595bb7]753        self._model_model.blockSignals(True)
754        # Modify font and foreground of affected rows
755        for column in range(0, self._model_model.columnCount()):
756            self._model_model.item(row, column).setForeground(brush)
757            self._model_model.item(row, column).setFont(font)
[eae226b]758            self._model_model.item(row, column).setEditable(fields_enabled)
[0595bb7]759        self._model_model.blockSignals(False)
760
[c5a2722f]761    def addConstraintToRow(self, constraint=None, row=0):
762        """
763        Adds the constraint object to requested row
764        """
765        # Create a new item and add the Constraint object as a child
[8e2cd79]766        assert isinstance(constraint, Constraint)
767        assert 0 <= row <= self._model_model.rowCount()
[00b7ddf0]768        assert self.isCheckable(row)
[c5a2722f]769
770        item = QtGui.QStandardItem()
771        item.setData(constraint)
772        self._model_model.item(row, 1).setChild(0, item)
773        # Set min/max to the value constrained
774        self.constraintAddedSignal.emit([row])
775        # Show visual hints for the constraint
776        font = QtGui.QFont()
777        font.setItalic(True)
778        brush = QtGui.QBrush(QtGui.QColor('blue'))
779        self.modifyViewOnRow(row, font=font, brush=brush)
780        self.communicate.statusBarUpdateSignal.emit('Constraint added')
781
[0595bb7]782    def addSimpleConstraint(self):
[7fd20fc]783        """
784        Adds a constraint on a single parameter.
[2add354]785        """
[d3c0b95]786        min_col = self.lstParams.itemDelegate().param_min
787        max_col = self.lstParams.itemDelegate().param_max
[0595bb7]788        for row in self.selectedParameters():
[00b7ddf0]789            assert(self.isCheckable(row))
[0595bb7]790            param = self._model_model.item(row, 0).text()
791            value = self._model_model.item(row, 1).text()
[235d766]792            min_t = self._model_model.item(row, min_col).text()
793            max_t = self._model_model.item(row, max_col).text()
[eae226b]794            # Create a Constraint object
[235d766]795            constraint = Constraint(param=param, value=value, min=min_t, max=max_t)
[eae226b]796            # Create a new item and add the Constraint object as a child
[0595bb7]797            item = QtGui.QStandardItem()
798            item.setData(constraint)
799            self._model_model.item(row, 1).setChild(0, item)
[235d766]800            # Assumed correctness from the validator
801            value = float(value)
802            # BUMPS calculates log(max-min) without any checks, so let's assign minor range
803            min_v = value - (value/10000.0)
804            max_v = value + (value/10000.0)
[eae226b]805            # Set min/max to the value constrained
[235d766]806            self._model_model.item(row, min_col).setText(str(min_v))
807            self._model_model.item(row, max_col).setText(str(max_v))
[be8f4b0]808            self.constraintAddedSignal.emit([row])
[eae226b]809            # Show visual hints for the constraint
810            font = QtGui.QFont()
811            font.setItalic(True)
812            brush = QtGui.QBrush(QtGui.QColor('blue'))
813            self.modifyViewOnRow(row, font=font, brush=brush)
[7fd20fc]814        self.communicate.statusBarUpdateSignal.emit('Constraint added')
815
[baeac95]816    def editConstraint(self):
817        """
818        Delete constraints from selected parameters.
819        """
820        params_list = [s.data() for s in self.lstParams.selectionModel().selectedRows()
821                   if self.isCheckable(s.row())]
822        assert len(params_list) == 1
823        row = self.lstParams.selectionModel().selectedRows()[0].row()
824        constraint = self.getConstraintForRow(row)
825        # Create and display the widget for param1 and param2
826        mc_widget = MultiConstraint(self, params=params_list, constraint=constraint)
827        # Check if any of the parameters are polydisperse
828        if not np.any([FittingUtilities.isParamPolydisperse(p, self.model_parameters, is2D=self.is2D) for p in params_list]):
829            # no parameters are pd - reset the text to not show the warning
830            mc_widget.lblWarning.setText("")
831        if mc_widget.exec_() != QtWidgets.QDialog.Accepted:
832            return
833
834        constraint = Constraint()
835        c_text = mc_widget.txtConstraint.text()
836
837        # widget.params[0] is the parameter we're constraining
838        constraint.param = mc_widget.params[0]
839        # parameter should have the model name preamble
840        model_name = self.kernel_module.name
841        # param_used is the parameter we're using in constraining function
842        param_used = mc_widget.params[1]
843        # Replace param_used with model_name.param_used
844        updated_param_used = model_name + "." + param_used
845        # Update constraint with new values
846        constraint.func = c_text
847        constraint.value_ex = updated_param_used
848        constraint.value = param_used
[09e0c32]849        # Should the new constraint be validated?
850        constraint.validate = mc_widget.validate
[baeac95]851
852        # Which row is the constrained parameter in?
853        row = self.getRowFromName(constraint.param)
854
855        # Create a new item and add the Constraint object as a child
856        self.addConstraintToRow(constraint=constraint, row=row)
857
[eae226b]858    def deleteConstraint(self):
859        """
860        Delete constraints from selected parameters.
861        """
[8e2cd79]862        params = [s.data() for s in self.lstParams.selectionModel().selectedRows()
[3b3b40b]863                   if self.isCheckable(s.row())]
864        for param in params:
865            self.deleteConstraintOnParameter(param=param)
[be8f4b0]866
867    def deleteConstraintOnParameter(self, param=None):
868        """
869        Delete the constraint on model parameter 'param'
870        """
[d3c0b95]871        min_col = self.lstParams.itemDelegate().param_min
872        max_col = self.lstParams.itemDelegate().param_max
[be8f4b0]873        for row in range(self._model_model.rowCount()):
[00b7ddf0]874            if not self.isCheckable(row):
875                continue
[3b3b40b]876            if not self.rowHasConstraint(row):
877                continue
[eae226b]878            # Get the Constraint object from of the model item
879            item = self._model_model.item(row, 1)
[3b3b40b]880            constraint = self.getConstraintForRow(row)
[d3c0b95]881            if constraint is None:
882                continue
883            if not isinstance(constraint, Constraint):
884                continue
[be8f4b0]885            if param and constraint.param != param:
886                continue
887            # Now we got the right row. Delete the constraint and clean up
[eae226b]888            # Retrieve old values and put them on the model
889            if constraint.min is not None:
[d3c0b95]890                self._model_model.item(row, min_col).setText(constraint.min)
[eae226b]891            if constraint.max is not None:
[d3c0b95]892                self._model_model.item(row, max_col).setText(constraint.max)
[eae226b]893            # Remove constraint item
894            item.removeRow(0)
[be8f4b0]895            self.constraintAddedSignal.emit([row])
[eae226b]896            self.modifyViewOnRow(row)
[be8f4b0]897
[eae226b]898        self.communicate.statusBarUpdateSignal.emit('Constraint removed')
[be8f4b0]899
[d3c0b95]900    def getConstraintForRow(self, row):
901        """
[c4b23dd]902        For the given row, return its constraint, if any (otherwise None)
[d3c0b95]903        """
[c4b23dd]904        if not self.isCheckable(row):
905            return None
906        item = self._model_model.item(row, 1)
907        try:
908            return item.child(0).data()
909        except AttributeError:
910            return None
[d3c0b95]911
[ecc5d043]912    def allParamNames(self):
913        """
914        Returns a list of all parameter names defined on the current model
915        """
916        all_params = self.kernel_module._model_info.parameters.kernel_parameters
917        all_param_names = [param.name for param in all_params]
918        # Assure scale and background are always included
919        if 'scale' not in all_param_names:
920            all_param_names.append('scale')
921        if 'background' not in all_param_names:
922            all_param_names.append('background')
923        return all_param_names
924
925    def paramHasConstraint(self, param=None):
926        """
927        Finds out if the given parameter in the main model has a constraint child
928        """
929        if param is None: return False
930        if param not in self.allParamNames(): return False
931
932        for row in range(self._model_model.rowCount()):
933            if self._model_model.item(row,0).text() != param: continue
934            return self.rowHasConstraint(row)
935
936        # nothing found
937        return False
938
[eae226b]939    def rowHasConstraint(self, row):
940        """
941        Finds out if row of the main model has a constraint child
942        """
[c4b23dd]943        if not self.isCheckable(row):
944            return False
945        item = self._model_model.item(row, 1)
946        if not item.hasChildren():
947            return False
948        c = item.child(0).data()
949        if isinstance(c, Constraint):
950            return True
[be8f4b0]951        return False
[116dd4c1]952
953    def rowHasActiveConstraint(self, row):
954        """
955        Finds out if row of the main model has an active constraint child
956        """
[c4b23dd]957        if not self.isCheckable(row):
958            return False
959        item = self._model_model.item(row, 1)
960        if not item.hasChildren():
961            return False
962        c = item.child(0).data()
963        if isinstance(c, Constraint) and c.active:
964            return True
[235d766]965        return False
966
967    def rowHasActiveComplexConstraint(self, row):
968        """
969        Finds out if row of the main model has an active, nontrivial constraint child
970        """
[c4b23dd]971        if not self.isCheckable(row):
972            return False
973        item = self._model_model.item(row, 1)
974        if not item.hasChildren():
975            return False
976        c = item.child(0).data()
977        if isinstance(c, Constraint) and c.func and c.active:
978            return True
[116dd4c1]979        return False
[eae226b]980
[7fd20fc]981    def selectParameters(self):
982        """
[d3c0b95]983        Selected parameter is chosen for fitting
[7fd20fc]984        """
985        status = QtCore.Qt.Checked
986        self.setParameterSelection(status)
987
988    def deselectParameters(self):
989        """
990        Selected parameters are removed for fitting
991        """
992        status = QtCore.Qt.Unchecked
993        self.setParameterSelection(status)
994
995    def selectedParameters(self):
996        """ Returns list of selected (highlighted) parameters """
[d3c0b95]997        return [s.row() for s in self.lstParams.selectionModel().selectedRows()
998                if self.isCheckable(s.row())]
[7fd20fc]999
1000    def setParameterSelection(self, status=QtCore.Qt.Unchecked):
1001        """
1002        Selected parameters are chosen for fitting
1003        """
1004        # Convert to proper indices and set requested enablement
1005        for row in self.selectedParameters():
1006            self._model_model.item(row, 0).setCheckState(status)
[d3c0b95]1007
1008    def getConstraintsForModel(self):
1009        """
1010        Return a list of tuples. Each tuple contains constraints mapped as
1011        ('constrained parameter', 'function to constrain')
1012        e.g. [('sld','5*sld_solvent')]
1013        """
1014        param_number = self._model_model.rowCount()
1015        params = [(self._model_model.item(s, 0).text(),
[c5a2722f]1016                    self._model_model.item(s, 1).child(0).data().func)
[116dd4c1]1017                    for s in range(param_number) if self.rowHasActiveConstraint(s)]
[d3c0b95]1018        return params
[7fd20fc]1019
[235d766]1020    def getComplexConstraintsForModel(self):
1021        """
1022        Return a list of tuples. Each tuple contains constraints mapped as
1023        ('constrained parameter', 'function to constrain')
[06234fc]1024        e.g. [('sld','5*M2.sld_solvent')].
[235d766]1025        Only for constraints with defined VALUE
1026        """
1027        param_number = self._model_model.rowCount()
1028        params = [(self._model_model.item(s, 0).text(),
1029                    self._model_model.item(s, 1).child(0).data().func)
1030                    for s in range(param_number) if self.rowHasActiveComplexConstraint(s)]
1031        return params
1032
[ba01ad1]1033    def getConstraintObjectsForModel(self):
1034        """
1035        Returns Constraint objects present on the whole model
1036        """
1037        param_number = self._model_model.rowCount()
1038        constraints = [self._model_model.item(s, 1).child(0).data()
1039                       for s in range(param_number) if self.rowHasConstraint(s)]
1040
1041        return constraints
1042
[3b3b40b]1043    def getConstraintsForFitting(self):
1044        """
1045        Return a list of constraints in format ready for use in fiting
1046        """
1047        # Get constraints
1048        constraints = self.getComplexConstraintsForModel()
1049        # See if there are any constraints across models
1050        multi_constraints = [cons for cons in constraints if self.isConstraintMultimodel(cons[1])]
1051
1052        if multi_constraints:
1053            # Let users choose what to do
1054            msg = "The current fit contains constraints relying on other fit pages.\n"
1055            msg += "Parameters with those constraints are:\n" +\
1056                '\n'.join([cons[0] for cons in multi_constraints])
1057            msg += "\n\nWould you like to remove these constraints or cancel fitting?"
1058            msgbox = QtWidgets.QMessageBox(self)
1059            msgbox.setIcon(QtWidgets.QMessageBox.Warning)
1060            msgbox.setText(msg)
1061            msgbox.setWindowTitle("Existing Constraints")
1062            # custom buttons
1063            button_remove = QtWidgets.QPushButton("Remove")
1064            msgbox.addButton(button_remove, QtWidgets.QMessageBox.YesRole)
1065            button_cancel = QtWidgets.QPushButton("Cancel")
1066            msgbox.addButton(button_cancel, QtWidgets.QMessageBox.RejectRole)
1067            retval = msgbox.exec_()
1068            if retval == QtWidgets.QMessageBox.RejectRole:
1069                # cancel fit
1070                raise ValueError("Fitting cancelled")
1071            else:
1072                # remove constraint
1073                for cons in multi_constraints:
1074                    self.deleteConstraintOnParameter(param=cons[0])
1075                # re-read the constraints
1076                constraints = self.getComplexConstraintsForModel()
1077
1078        return constraints
1079
[7fd20fc]1080    def showModelDescription(self):
1081        """
1082        Creates a window with model description, when right clicked in the treeview
[2add354]1083        """
1084        msg = 'Model description:\n'
1085        if self.kernel_module is not None:
1086            if str(self.kernel_module.description).rstrip().lstrip() == '':
1087                msg += "Sorry, no information is available for this model."
1088            else:
1089                msg += self.kernel_module.description + '\n'
1090        else:
1091            msg += "You must select a model to get information on this"
1092
[4992ff2]1093        menu = QtWidgets.QMenu()
1094        label = QtWidgets.QLabel(msg)
[d6b8a1d]1095        action = QtWidgets.QWidgetAction(self)
[672b8ab]1096        action.setDefaultWidget(label)
1097        menu.addAction(action)
[7fd20fc]1098        return menu
[2add354]1099
[a98014ab]1100    def canHaveMagnetism(self):
1101        """
1102        Checks if the current model has magnetic scattering implemented
1103        """
[48df831]1104        has_mag_params = False
[ec4a143]1105        if self.kernel_module:
1106            has_mag_params = len(self.kernel_module.magnetic_params) > 0
1107        return self.is2D and has_mag_params
[a98014ab]1108
[0268aed]1109    def onSelectModel(self):
[cbcdd2c]1110        """
[0268aed]1111        Respond to select Model from list event
[cbcdd2c]1112        """
[d6b8a1d]1113        model = self.cbModel.currentText()
[0268aed]1114
[2eeda93]1115        if model == MODEL_DEFAULT:
1116            # if the previous category was not the default, keep it.
1117            # Otherwise, just return
1118            if self._previous_model_index != 0:
1119                # We need to block signals, or else state changes on perceived unchanged conditions
1120                self.cbModel.blockSignals(True)
1121                self.cbModel.setCurrentIndex(self._previous_model_index)
1122                self.cbModel.blockSignals(False)
1123            return
1124
[f4aa7a8]1125        # Assure the control is active
1126        if not self.cbModel.isEnabled():
1127            return
1128        # Empty combobox forced to be read
[b3e8629]1129        if not model:
1130            return
[a98014ab]1131
1132        self.chkMagnetism.setEnabled(self.canHaveMagnetism())
[2eeda93]1133        self.chkMagnetism.setEnabled(self.canHaveMagnetism())
[a98014ab]1134        self.tabFitting.setTabEnabled(TAB_MAGNETISM, self.chkMagnetism.isChecked() and self.canHaveMagnetism())
[2eeda93]1135        self._previous_model_index = self.cbModel.currentIndex()
[a98014ab]1136
[f182f93]1137        # Reset parameters to fit
[6dbff18]1138        self.resetParametersToFit()
[d7ff531]1139        self.has_error_column = False
[aca8418]1140        self.has_poly_error_column = False
[f182f93]1141
[605d944]1142        structure = None
1143        if self.cbStructureFactor.isEnabled():
1144            structure = str(self.cbStructureFactor.currentText())
1145        self.respondToModelStructure(model=model, structure_factor=structure)
[fd1ae6d1]1146
[ee18d33]1147    def onSelectBatchFilename(self, data_index):
1148        """
1149        Update the logic based on the selected file in batch fitting
1150        """
[d4dac80]1151        self.data_index = data_index
[ee18d33]1152        self.updateQRange()
1153
[fd1ae6d1]1154    def onSelectStructureFactor(self):
1155        """
1156        Select Structure Factor from list
1157        """
1158        model = str(self.cbModel.currentText())
1159        category = str(self.cbCategory.currentText())
1160        structure = str(self.cbStructureFactor.currentText())
1161        if category == CATEGORY_STRUCTURE:
1162            model = None
[e11106e]1163
1164        # Reset parameters to fit
[6dbff18]1165        self.resetParametersToFit()
[e11106e]1166        self.has_error_column = False
1167        self.has_poly_error_column = False
1168
[fd1ae6d1]1169        self.respondToModelStructure(model=model, structure_factor=structure)
1170
[6dbff18]1171    def resetParametersToFit(self):
1172        """
1173        Clears the list of parameters to be fitted
1174        """
1175        self.main_params_to_fit = []
1176        self.poly_params_to_fit = []
1177        self.magnet_params_to_fit = []
1178
[3b3b40b]1179    def onCustomModelChange(self):
1180        """
1181        Reload the custom model combobox
1182        """
1183        self.custom_models = self.customModels()
1184        self.readCustomCategoryInfo()
[2dce1d8]1185        self.onCategoriesChanged()
1186
[3b3b40b]1187        # See if we need to update the combo in-place
1188        if self.cbCategory.currentText() != CATEGORY_CUSTOM: return
1189
1190        current_text = self.cbModel.currentText()
1191        self.cbModel.blockSignals(True)
1192        self.cbModel.clear()
1193        self.cbModel.blockSignals(False)
1194        self.enableModelCombo()
1195        self.disableStructureCombo()
1196        # Retrieve the list of models
1197        model_list = self.master_category_dict[CATEGORY_CUSTOM]
1198        # Populate the models combobox
1199        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
1200        new_index = self.cbModel.findText(current_text)
1201        if new_index != -1:
1202            self.cbModel.setCurrentIndex(self.cbModel.findText(current_text))
1203
1204    def onSelectionChanged(self):
1205        """
1206        React to parameter selection
1207        """
1208        rows = self.lstParams.selectionModel().selectedRows()
1209        # Clean previous messages
1210        self.communicate.statusBarUpdateSignal.emit("")
1211        if len(rows) == 1:
1212            # Show constraint, if present
1213            row = rows[0].row()
[c4b23dd]1214            if not self.rowHasConstraint(row):
1215                return
[d1e4689]1216            constr = self.getConstraintForRow(row)
[c4b23dd]1217            func = self.getConstraintForRow(row).func
[d1e4689]1218            if constr.func is not None:
1219                # inter-parameter constraint
1220                update_text = "Active constraint: "+func
1221            elif constr.param == rows[0].data():
1222                # current value constraint
1223                update_text = "Value constrained to: " + str(constr.value)
1224            else:
1225                # ill defined constraint
1226                return
1227            self.communicate.statusBarUpdateSignal.emit(update_text)
[3b3b40b]1228
[47d7d2d]1229    def replaceConstraintName(self, old_name, new_name=""):
1230        """
1231        Replace names of models in defined constraints
1232        """
1233        param_number = self._model_model.rowCount()
1234        # loop over parameters
1235        for row in range(param_number):
1236            if self.rowHasConstraint(row):
1237                func = self._model_model.item(row, 1).child(0).data().func
1238                if old_name in func:
1239                    new_func = func.replace(old_name, new_name)
1240                    self._model_model.item(row, 1).child(0).data().func = new_func
1241
[3b3b40b]1242    def isConstraintMultimodel(self, constraint):
1243        """
1244        Check if the constraint function text contains current model name
1245        """
1246        current_model_name = self.kernel_module.name
1247        if current_model_name in constraint:
1248            return False
1249        else:
1250            return True
1251
[9c0ce68]1252    def updateData(self):
1253        """
1254        Helper function for recalculation of data used in plotting
1255        """
1256        # Update the chart
[0268aed]1257        if self.data_is_loaded:
[d48cc19]1258            self.cmdPlot.setText("Show Plot")
[0268aed]1259            self.calculateQGridForModel()
1260        else:
[d48cc19]1261            self.cmdPlot.setText("Calculate")
[0268aed]1262            # Create default datasets if no data passed
1263            self.createDefaultDataset()
[33d5956]1264            self.theory_item = None # ensure theory is recalc. before plot, see showTheoryPlot()
[0268aed]1265
[9c0ce68]1266    def respondToModelStructure(self, model=None, structure_factor=None):
1267        # Set enablement on calculate/plot
1268        self.cmdPlot.setEnabled(True)
1269
1270        # kernel parameters -> model_model
1271        self.SASModelToQModel(model, structure_factor)
1272
[ec4a143]1273        # Enable magnetism checkbox for selected models
1274        self.chkMagnetism.setEnabled(self.canHaveMagnetism())
1275        self.tabFitting.setTabEnabled(TAB_MAGNETISM, self.chkMagnetism.isChecked() and self.canHaveMagnetism())
1276
1277        # Update column widths
[f37cab0]1278        for column, width in self.lstParamHeaderSizes.items():
1279            self.lstParams.setColumnWidth(column, width)
1280
[9c0ce68]1281        # Update plot
1282        self.updateData()
1283
[6011788]1284        # Update state stack
[00b3b40]1285        self.updateUndo()
[2add354]1286
[be8f4b0]1287        # Let others know
1288        self.newModelSignal.emit()
1289
[cd31251]1290    def onSelectCategory(self):
[60af928]1291        """
1292        Select Category from list
1293        """
[d6b8a1d]1294        category = self.cbCategory.currentText()
[86f88d1]1295        # Check if the user chose "Choose category entry"
[4d457df]1296        if category == CATEGORY_DEFAULT:
[86f88d1]1297            # if the previous category was not the default, keep it.
1298            # Otherwise, just return
1299            if self._previous_category_index != 0:
[351b53e]1300                # We need to block signals, or else state changes on perceived unchanged conditions
1301                self.cbCategory.blockSignals(True)
[86f88d1]1302                self.cbCategory.setCurrentIndex(self._previous_category_index)
[351b53e]1303                self.cbCategory.blockSignals(False)
[86f88d1]1304            return
1305
[4d457df]1306        if category == CATEGORY_STRUCTURE:
[6f7f652]1307            self.disableModelCombo()
1308            self.enableStructureCombo()
[f4aa7a8]1309            # set the index to 0
1310            self.cbStructureFactor.setCurrentIndex(0)
1311            self.model_parameters = None
[29eb947]1312            self._model_model.clear()
[6f7f652]1313            return
[2eeda93]1314        # Wipe out the parameter model
1315        self._model_model.clear()
[cbcdd2c]1316        # Safely clear and enable the model combo
[6f7f652]1317        self.cbModel.blockSignals(True)
1318        self.cbModel.clear()
1319        self.cbModel.blockSignals(False)
1320        self.enableModelCombo()
1321        self.disableStructureCombo()
1322
[86f88d1]1323        self._previous_category_index = self.cbCategory.currentIndex()
[cbcdd2c]1324        # Retrieve the list of models
[4d457df]1325        model_list = self.master_category_dict[category]
[cbcdd2c]1326        # Populate the models combobox
[2eeda93]1327        self.cbModel.blockSignals(True)
1328        self.cbModel.addItem(MODEL_DEFAULT)
[b1e36a3]1329        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
[2eeda93]1330        self.cbModel.blockSignals(False)
[4d457df]1331
[d2007a8]1332    def onPolyModelChange(self, top, bottom):
[0268aed]1333        """
1334        Callback method for updating the main model and sasmodel
1335        parameters with the GUI values in the polydispersity view
1336        """
[d2007a8]1337        item = self._poly_model.itemFromIndex(top)
[0268aed]1338        model_column = item.column()
1339        model_row = item.row()
1340        name_index = self._poly_model.index(model_row, 0)
[287d356]1341        parameter_name = str(name_index.data()) # "distribution of sld" etc.
1342        if "istribution of" in parameter_name:
[358b39d]1343            # just the last word
1344            parameter_name = parameter_name.rsplit()[-1]
[c1e380e]1345
[906e0c7]1346        delegate = self.lstPoly.itemDelegate()
1347
[06b0138]1348        # Extract changed value.
[906e0c7]1349        if model_column == delegate.poly_parameter:
[00b3b40]1350            # Is the parameter checked for fitting?
[0268aed]1351            value = item.checkState()
[1643d8ed]1352            parameter_name = parameter_name + '.width'
[c1e380e]1353            if value == QtCore.Qt.Checked:
[6dbff18]1354                self.poly_params_to_fit.append(parameter_name)
[c1e380e]1355            else:
[6dbff18]1356                if parameter_name in self.poly_params_to_fit:
1357                    self.poly_params_to_fit.remove(parameter_name)
1358            self.cmdFit.setEnabled(self.haveParamsToFit())
[906e0c7]1359
1360        elif model_column in [delegate.poly_min, delegate.poly_max]:
[aca8418]1361            try:
[fbfc488]1362                value = GuiUtils.toDouble(item.text())
[0261bc1]1363            except TypeError:
[aca8418]1364                # Can't be converted properly, bring back the old value and exit
1365                return
1366
1367            current_details = self.kernel_module.details[parameter_name]
[906e0c7]1368            if self.has_poly_error_column:
1369                # err column changes the indexing
1370                current_details[model_column-2] = value
1371            else:
1372                current_details[model_column-1] = value
1373
1374        elif model_column == delegate.poly_function:
[919d47c]1375            # name of the function - just pass
[906e0c7]1376            pass
1377
[0268aed]1378        else:
1379            try:
[fbfc488]1380                value = GuiUtils.toDouble(item.text())
[0261bc1]1381            except TypeError:
[0268aed]1382                # Can't be converted properly, bring back the old value and exit
1383                return
1384
[aca8418]1385            # Update the sasmodel
1386            # PD[ratio] -> width, npts -> npts, nsigs -> nsigmas
[66d4370]1387            #self.kernel_module.setParam(parameter_name + '.' + delegate.columnDict()[model_column], value)
1388            key = parameter_name + '.' + delegate.columnDict()[model_column]
1389            self.poly_params[key] = value
[0268aed]1390
[9c0ce68]1391            # Update plot
1392            self.updateData()
1393
[906e0c7]1394        # update in param model
1395        if model_column in [delegate.poly_pd, delegate.poly_error, delegate.poly_min, delegate.poly_max]:
1396            row = self.getRowFromName(parameter_name)
[f3cc979]1397            param_item = self._model_model.item(row).child(0).child(0, model_column)
1398            if param_item is None:
1399                return
[73665a8]1400            self._model_model.blockSignals(True)
[f3cc979]1401            param_item.setText(item.text())
[73665a8]1402            self._model_model.blockSignals(False)
[906e0c7]1403
[d2007a8]1404    def onMagnetModelChange(self, top, bottom):
[b00414d]1405        """
1406        Callback method for updating the sasmodel magnetic parameters with the GUI values
1407        """
[d2007a8]1408        item = self._magnet_model.itemFromIndex(top)
[b00414d]1409        model_column = item.column()
1410        model_row = item.row()
1411        name_index = self._magnet_model.index(model_row, 0)
[fbfc488]1412        parameter_name = str(self._magnet_model.data(name_index))
[b00414d]1413
1414        if model_column == 0:
1415            value = item.checkState()
1416            if value == QtCore.Qt.Checked:
[6dbff18]1417                self.magnet_params_to_fit.append(parameter_name)
[b00414d]1418            else:
[6dbff18]1419                if parameter_name in self.magnet_params_to_fit:
1420                    self.magnet_params_to_fit.remove(parameter_name)
1421            self.cmdFit.setEnabled(self.haveParamsToFit())
[b00414d]1422            # Update state stack
1423            self.updateUndo()
1424            return
1425
1426        # Extract changed value.
1427        try:
[fbfc488]1428            value = GuiUtils.toDouble(item.text())
[0261bc1]1429        except TypeError:
[b00414d]1430            # Unparsable field
1431            return
[66d4370]1432        delegate = self.lstMagnetic.itemDelegate()
1433
1434        if model_column > 1:
1435            if model_column == delegate.mag_min:
1436                pos = 1
1437            elif model_column == delegate.mag_max:
1438                pos = 2
1439            elif model_column == delegate.mag_unit:
1440                pos = 0
1441            else:
1442                raise AttributeError("Wrong column in magnetism table.")
1443            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1444            self.kernel_module.details[parameter_name][pos] = value
1445        else:
1446            self.magnet_params[parameter_name] = value
1447            #self.kernel_module.setParam(parameter_name) = value
1448            # Force the chart update when actual parameters changed
[b00414d]1449            self.recalculatePlotData()
1450
1451        # Update state stack
1452        self.updateUndo()
1453
[2add354]1454    def onHelp(self):
1455        """
1456        Show the "Fitting" section of help
1457        """
[aed0532]1458        tree_location = "/user/qtgui/Perspectives/Fitting/"
[70080a0]1459
1460        # Actual file will depend on the current tab
1461        tab_id = self.tabFitting.currentIndex()
1462        helpfile = "fitting.html"
1463        if tab_id == 0:
[9c05f22]1464            # Look at the model and if set, pull out its help page
1465            if self.kernel_module is not None and hasattr(self.kernel_module, 'name'):
1466                # See if the help file is there
1467                # This breaks encapsulation a bit, though.
1468                full_path = GuiUtils.HELP_DIRECTORY_LOCATION
1469                sas_path = os.path.abspath(os.path.dirname(sys.argv[0]))
1470                location = sas_path + "/" + full_path
1471                location += "/user/models/" + self.kernel_module.id + ".html"
1472                if os.path.isfile(location):
1473                    # We have HTML for this model - show it
1474                    tree_location = "/user/models/"
1475                    helpfile = self.kernel_module.id + ".html"
1476            else:
1477                helpfile = "fitting_help.html"
[70080a0]1478        elif tab_id == 1:
1479            helpfile = "residuals_help.html"
1480        elif tab_id == 2:
[e90988c]1481            helpfile = "resolution.html"
[70080a0]1482        elif tab_id == 3:
[e90988c]1483            helpfile = "pd/polydispersity.html"
[70080a0]1484        elif tab_id == 4:
[e90988c]1485            helpfile = "magnetism/magnetism.html"
[70080a0]1486        help_location = tree_location + helpfile
[d6b8a1d]1487
[e90988c]1488        self.showHelp(help_location)
1489
1490    def showHelp(self, url):
1491        """
1492        Calls parent's method for opening an HTML page
1493        """
1494        self.parent.showHelp(url)
[2add354]1495
[6ff2eb3]1496    def onDisplayMagneticAngles(self):
1497        """
1498        Display a simple image showing direction of magnetic angles
1499        """
1500        self.magneticAnglesWidget.show()
1501
[0268aed]1502    def onFit(self):
1503        """
1504        Perform fitting on the current data
1505        """
[ded5e77]1506        if self.fit_started:
1507            self.stopFit()
1508            return
1509
[116dd4c1]1510        # initialize fitter constants
[f182f93]1511        fit_id = 0
1512        handler = None
1513        batch_inputs = {}
1514        batch_outputs = {}
1515        #---------------------------------
[14ec91c5]1516        if LocalConfig.USING_TWISTED:
[7adc2a8]1517            handler = None
1518            updater = None
1519        else:
1520            handler = ConsoleUpdate(parent=self.parent,
1521                                    manager=self,
1522                                    improvement_delta=0.1)
1523            updater = handler.update_fit
[f182f93]1524
[116dd4c1]1525        # Prepare the fitter object
[c6343a5]1526        try:
1527            fitters, _ = self.prepareFitters()
1528        except ValueError as ex:
1529            # This should not happen! GUI explicitly forbids this situation
[3b3b40b]1530            self.communicate.statusBarUpdateSignal.emit(str(ex))
[c6343a5]1531            return
[f182f93]1532
[d4dac80]1533        # keep local copy of kernel parameters, as they will change during the update
1534        self.kernel_module_copy = copy.deepcopy(self.kernel_module)
1535
[f182f93]1536        # Create the fitting thread, based on the fitter
[3b3b40b]1537        completefn = self.batchFittingCompleted if self.is_batch_fitting else self.fittingCompleted
[ee18d33]1538
[ded5e77]1539        self.calc_fit = FitThread(handler=handler,
[116dd4c1]1540                            fn=fitters,
1541                            batch_inputs=batch_inputs,
1542                            batch_outputs=batch_outputs,
1543                            page_id=[[self.page_id]],
1544                            updatefn=updater,
[91ad45c]1545                            completefn=completefn,
1546                            reset_flag=self.is_chain_fitting)
[7adc2a8]1547
[14ec91c5]1548        if LocalConfig.USING_TWISTED:
[7adc2a8]1549            # start the trhrhread with twisted
[ded5e77]1550            calc_thread = threads.deferToThread(self.calc_fit.compute)
[14ec91c5]1551            calc_thread.addCallback(completefn)
[7adc2a8]1552            calc_thread.addErrback(self.fitFailed)
1553        else:
1554            # Use the old python threads + Queue
[ded5e77]1555            self.calc_fit.queue()
1556            self.calc_fit.ready(2.5)
[f182f93]1557
[d7ff531]1558        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
[ded5e77]1559        self.fit_started = True
[557fc498]1560
[14ec91c5]1561        # Disable some elements
[557fc498]1562        self.disableInteractiveElements()
[0268aed]1563
[ded5e77]1564    def stopFit(self):
1565        """
1566        Attempt to stop the fitting thread
1567        """
1568        if self.calc_fit is None or not self.calc_fit.isrunning():
1569            return
1570        self.calc_fit.stop()
1571        #re-enable the Fit button
[557fc498]1572        self.enableInteractiveElements()
[ded5e77]1573
1574        msg = "Fitting cancelled."
1575        self.communicate.statusBarUpdateSignal.emit(msg)
1576
[f182f93]1577    def updateFit(self):
1578        """
1579        """
[b3e8629]1580        print("UPDATE FIT")
[0268aed]1581        pass
1582
[02ddfb4]1583    def fitFailed(self, reason):
1584        """
1585        """
[557fc498]1586        self.enableInteractiveElements()
[ded5e77]1587        msg = "Fitting failed with: "+ str(reason)
1588        self.communicate.statusBarUpdateSignal.emit(msg)
[02ddfb4]1589
[3b3b40b]1590    def batchFittingCompleted(self, result):
1591        """
1592        Send the finish message from calculate threads to main thread
1593        """
[a2cc8b97]1594        if result is None:
1595            result = tuple()
[3b3b40b]1596        self.batchFittingFinishedSignal.emit(result)
1597
[ee18d33]1598    def batchFitComplete(self, result):
1599        """
1600        Receive and display batch fitting results
1601        """
1602        #re-enable the Fit button
[557fc498]1603        self.enableInteractiveElements()
[d4dac80]1604
[a2cc8b97]1605        if len(result) == 0:
[d4dac80]1606            msg = "Fitting failed."
1607            self.communicate.statusBarUpdateSignal.emit(msg)
1608            return
1609
[3b3b40b]1610        # Show the grid panel
[c4c4957]1611        page_name = "BatchPage" + str(self.tab_id)
1612        results = copy.deepcopy(result[0])
1613        results.append(page_name)
1614        self.communicate.sendDataToGridSignal.emit(results)
[d4dac80]1615
1616        elapsed = result[1]
1617        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
1618        self.communicate.statusBarUpdateSignal.emit(msg)
1619
1620        # Run over the list of results and update the items
1621        for res_index, res_list in enumerate(result[0]):
1622            # results
1623            res = res_list[0]
1624            param_dict = self.paramDictFromResults(res)
1625
1626            # create local kernel_module
1627            kernel_module = FittingUtilities.updateKernelWithResults(self.kernel_module, param_dict)
1628            # pull out current data
1629            data = self._logic[res_index].data
1630
1631            # Switch indexes
1632            self.onSelectBatchFilename(res_index)
1633
1634            method = self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1635            self.calculateQGridForModelExt(data=data, model=kernel_module, completefn=method, use_threads=False)
1636
1637        # Restore original kernel_module, so subsequent fits on the same model don't pick up the new params
1638        if self.kernel_module is not None:
1639            self.kernel_module = copy.deepcopy(self.kernel_module_copy)
1640
1641    def paramDictFromResults(self, results):
1642        """
1643        Given the fit results structure, pull out optimized parameters and return them as nicely
1644        formatted dict
1645        """
1646        if results.fitness is None or \
1647            not np.isfinite(results.fitness) or \
1648            np.any(results.pvec is None) or \
1649            not np.all(np.isfinite(results.pvec)):
1650            msg = "Fitting did not converge!"
1651            self.communicate.statusBarUpdateSignal.emit(msg)
1652            msg += results.mesg
[bb477f5]1653            logger.error(msg)
[d4dac80]1654            return
1655
1656        param_list = results.param_list # ['radius', 'radius.width']
1657        param_values = results.pvec     # array([ 0.36221662,  0.0146783 ])
1658        param_stderr = results.stderr   # array([ 1.71293015,  1.71294233])
1659        params_and_errors = list(zip(param_values, param_stderr))
1660        param_dict = dict(zip(param_list, params_and_errors))
1661
1662        return param_dict
[3b3b40b]1663
1664    def fittingCompleted(self, result):
1665        """
1666        Send the finish message from calculate threads to main thread
1667        """
[a2cc8b97]1668        if result is None:
1669            result = tuple()
[3b3b40b]1670        self.fittingFinishedSignal.emit(result)
[ee18d33]1671
[f182f93]1672    def fitComplete(self, result):
1673        """
1674        Receive and display fitting results
1675        "result" is a tuple of actual result list and the fit time in seconds
1676        """
1677        #re-enable the Fit button
[557fc498]1678        self.enableInteractiveElements()
[d7ff531]1679
[a2cc8b97]1680        if len(result) == 0:
[3b3b40b]1681            msg = "Fitting failed."
[06234fc]1682            self.communicate.statusBarUpdateSignal.emit(msg)
1683            return
[d7ff531]1684
[ee18d33]1685        res_list = result[0][0]
[f182f93]1686        res = res_list[0]
[d4dac80]1687        self.chi2 = res.fitness
1688        param_dict = self.paramDictFromResults(res)
[f182f93]1689
[14acf92]1690        if param_dict is None:
1691            return
[0c83303]1692        self.communicate.resultPlotUpdateSignal.emit(result[0])
[14acf92]1693
[f182f93]1694        elapsed = result[1]
[9d130f3]1695        if self.calc_fit is not None and self.calc_fit._interrupting:
[ded5e77]1696            msg = "Fitting cancelled by user after: %s s." % GuiUtils.formatNumber(elapsed)
[bb477f5]1697            logger.warning("\n"+msg+"\n")
[ded5e77]1698        else:
1699            msg = "Fitting completed successfully in: %s s." % GuiUtils.formatNumber(elapsed)
[f182f93]1700        self.communicate.statusBarUpdateSignal.emit(msg)
1701
1702        # Dictionary of fitted parameter: value, error
1703        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
1704        self.updateModelFromList(param_dict)
1705
[aca8418]1706        self.updatePolyModelFromList(param_dict)
1707
[b00414d]1708        self.updateMagnetModelFromList(param_dict)
1709
[d7ff531]1710        # update charts
1711        self.onPlot()
[c2f3ca2]1712        #self.recalculatePlotData()
1713
[d7ff531]1714
[f182f93]1715        # Read only value - we can get away by just printing it here
[2add354]1716        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
[f182f93]1717        self.lblChi2Value.setText(chi2_repr)
1718
[116dd4c1]1719    def prepareFitters(self, fitter=None, fit_id=0):
1720        """
1721        Prepare the Fitter object for use in fitting
1722        """
1723        # fitter = None -> single/batch fitting
1724        # fitter = Fit() -> simultaneous fitting
1725
1726        # Data going in
1727        data = self.logic.data
[66d4370]1728        model = copy.deepcopy(self.kernel_module)
[116dd4c1]1729        qmin = self.q_range_min
1730        qmax = self.q_range_max
[66d4370]1731        # add polydisperse/magnet parameters if asked
1732        self.updateKernelModelWithExtraParams(model)
[6dbff18]1733
[d4216ac]1734        params_to_fit = copy.deepcopy(self.main_params_to_fit)
[6dbff18]1735        if self.chkPolydispersity.isChecked():
1736            params_to_fit += self.poly_params_to_fit
[a98014ab]1737        if self.chkMagnetism.isChecked() and self.canHaveMagnetism():
[6dbff18]1738            params_to_fit += self.magnet_params_to_fit
[8e2cd79]1739        if not params_to_fit:
[c6343a5]1740            raise ValueError('Fitting requires at least one parameter to optimize.')
[116dd4c1]1741
[8b480d27]1742        # Get the constraints.
1743        constraints = self.getComplexConstraintsForModel()
1744        if fitter is None:
1745            # For single fits - check for inter-model constraints
1746            constraints = self.getConstraintsForFitting()
[3b3b40b]1747
[9a7c81c]1748        smearer = self.smearing_widget.smearer()
[116dd4c1]1749        handler = None
1750        batch_inputs = {}
1751        batch_outputs = {}
1752
1753        fitters = []
1754        for fit_index in self.all_data:
1755            fitter_single = Fit() if fitter is None else fitter
1756            data = GuiUtils.dataFromItem(fit_index)
[9a7c81c]1757            # Potential weights added directly to data
[b764ae5]1758            weighted_data = self.addWeightingToData(data)
[116dd4c1]1759            try:
[b764ae5]1760                fitter_single.set_model(model, fit_id, params_to_fit, data=weighted_data,
[116dd4c1]1761                             constraints=constraints)
1762            except ValueError as ex:
[3b3b40b]1763                raise ValueError("Setting model parameters failed with: %s" % ex)
[116dd4c1]1764
[b764ae5]1765            qmin, qmax, _ = self.logic.computeRangeFromData(weighted_data)
1766            fitter_single.set_data(data=weighted_data, id=fit_id, smearer=smearer, qmin=qmin,
[116dd4c1]1767                            qmax=qmax)
1768            fitter_single.select_problem_for_fit(id=fit_id, value=1)
1769            if fitter is None:
1770                # Assign id to the new fitter only
1771                fitter_single.fitter_id = [self.page_id]
1772            fit_id += 1
1773            fitters.append(fitter_single)
1774
1775        return fitters, fit_id
1776
[f182f93]1777    def iterateOverModel(self, func):
1778        """
1779        Take func and throw it inside the model row loop
1780        """
[b3e8629]1781        for row_i in range(self._model_model.rowCount()):
[f182f93]1782            func(row_i)
1783
1784    def updateModelFromList(self, param_dict):
1785        """
1786        Update the model with new parameters, create the errors column
1787        """
1788        assert isinstance(param_dict, dict)
1789        if not dict:
1790            return
1791
[919d47c]1792        def updateFittedValues(row):
[f182f93]1793            # Utility function for main model update
[d7ff531]1794            # internal so can use closure for param_dict
[919d47c]1795            param_name = str(self._model_model.item(row, 0).text())
[00b7ddf0]1796            if not self.isCheckable(row) or param_name not in list(param_dict.keys()):
[f182f93]1797                return
1798            # modify the param value
[454670d]1799            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
[919d47c]1800            self._model_model.item(row, 1).setText(param_repr)
[c2f3ca2]1801            self.kernel_module.setParam(param_name, param_dict[param_name][0])
[f182f93]1802            if self.has_error_column:
[454670d]1803                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
[919d47c]1804                self._model_model.item(row, 2).setText(error_repr)
[f182f93]1805
[919d47c]1806        def updatePolyValues(row):
1807            # Utility function for updateof polydispersity part of the main model
1808            param_name = str(self._model_model.item(row, 0).text())+'.width'
[00b7ddf0]1809            if not self.isCheckable(row) or param_name not in list(param_dict.keys()):
[919d47c]1810                return
1811            # modify the param value
1812            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1813            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
[906e0c7]1814            # modify the param error
1815            if self.has_error_column:
1816                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1817                self._model_model.item(row, 0).child(0).child(0,2).setText(error_repr)
[919d47c]1818
1819        def createErrorColumn(row):
[f182f93]1820            # Utility function for error column update
1821            item = QtGui.QStandardItem()
[919d47c]1822            def createItem(param_name):
[f182f93]1823                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1824                item.setText(error_repr)
[919d47c]1825            def curr_param():
1826                return str(self._model_model.item(row, 0).text())
1827
[b3e8629]1828            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
[919d47c]1829
[f182f93]1830            error_column.append(item)
1831
[906e0c7]1832        def createPolyErrorColumn(row):
1833            # Utility function for error column update in the polydispersity sub-rows
1834            # NOTE: only creates empty items; updatePolyValues adds the error value
1835            item = self._model_model.item(row, 0)
1836            if not item.hasChildren():
1837                return
1838            poly_item = item.child(0)
1839            if not poly_item.hasChildren():
1840                return
1841            poly_item.insertColumn(2, [QtGui.QStandardItem("")])
1842
1843        if not self.has_error_column:
1844            # create top-level error column
1845            error_column = []
1846            self.lstParams.itemDelegate().addErrorColumn()
1847            self.iterateOverModel(createErrorColumn)
1848
1849            self._model_model.insertColumn(2, error_column)
1850
1851            FittingUtilities.addErrorHeadersToModel(self._model_model)
1852
1853            # create error column in polydispersity sub-rows
1854            self.iterateOverModel(createPolyErrorColumn)
1855
1856            self.has_error_column = True
1857
[c2f3ca2]1858        # block signals temporarily, so we don't end up
1859        # updating charts with every single model change on the end of fitting
[d2007a8]1860        self._model_model.dataChanged.disconnect()
[d7ff531]1861        self.iterateOverModel(updateFittedValues)
[919d47c]1862        self.iterateOverModel(updatePolyValues)
[d2007a8]1863        self._model_model.dataChanged.connect(self.onMainParamsChange)
[f182f93]1864
[8e2cd79]1865    def iterateOverPolyModel(self, func):
1866        """
1867        Take func and throw it inside the poly model row loop
1868        """
1869        for row_i in range(self._poly_model.rowCount()):
1870            func(row_i)
1871
[aca8418]1872    def updatePolyModelFromList(self, param_dict):
1873        """
1874        Update the polydispersity model with new parameters, create the errors column
1875        """
1876        assert isinstance(param_dict, dict)
1877        if not dict:
1878            return
1879
1880        def updateFittedValues(row_i):
1881            # Utility function for main model update
1882            # internal so can use closure for param_dict
1883            if row_i >= self._poly_model.rowCount():
1884                return
1885            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
[b3e8629]1886            if param_name not in list(param_dict.keys()):
[aca8418]1887                return
1888            # modify the param value
1889            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1890            self._poly_model.item(row_i, 1).setText(param_repr)
[c2f3ca2]1891            self.kernel_module.setParam(param_name, param_dict[param_name][0])
[aca8418]1892            if self.has_poly_error_column:
1893                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1894                self._poly_model.item(row_i, 2).setText(error_repr)
1895
1896        def createErrorColumn(row_i):
1897            # Utility function for error column update
1898            if row_i >= self._poly_model.rowCount():
1899                return
1900            item = QtGui.QStandardItem()
[919d47c]1901
1902            def createItem(param_name):
[aca8418]1903                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1904                item.setText(error_repr)
[919d47c]1905
1906            def poly_param():
1907                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1908
[b3e8629]1909            [createItem(param_name) for param_name in list(param_dict.keys()) if poly_param() == param_name]
[919d47c]1910
[aca8418]1911            error_column.append(item)
1912
1913        # block signals temporarily, so we don't end up
1914        # updating charts with every single model change on the end of fitting
[d2007a8]1915        self._poly_model.dataChanged.disconnect()
[8e2cd79]1916        self.iterateOverPolyModel(updateFittedValues)
[d2007a8]1917        self._poly_model.dataChanged.connect(self.onPolyModelChange)
[aca8418]1918
1919        if self.has_poly_error_column:
1920            return
1921
[8eaa101]1922        self.lstPoly.itemDelegate().addErrorColumn()
[aca8418]1923        error_column = []
[8e2cd79]1924        self.iterateOverPolyModel(createErrorColumn)
[aca8418]1925
1926        # switch off reponse to model change
1927        self._poly_model.insertColumn(2, error_column)
1928        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
1929
1930        self.has_poly_error_column = True
1931
[8e2cd79]1932    def iterateOverMagnetModel(self, func):
1933        """
1934        Take func and throw it inside the magnet model row loop
1935        """
[30339cc]1936        for row_i in range(self._magnet_model.rowCount()):
[8e2cd79]1937            func(row_i)
1938
[b00414d]1939    def updateMagnetModelFromList(self, param_dict):
1940        """
1941        Update the magnetic model with new parameters, create the errors column
1942        """
1943        assert isinstance(param_dict, dict)
1944        if not dict:
1945            return
[3b3b40b]1946        if self._magnet_model.rowCount() == 0:
[cee5c78]1947            return
[b00414d]1948
1949        def updateFittedValues(row):
1950            # Utility function for main model update
1951            # internal so can use closure for param_dict
[cee5c78]1952            if self._magnet_model.item(row, 0) is None:
1953                return
[b00414d]1954            param_name = str(self._magnet_model.item(row, 0).text())
[b3e8629]1955            if param_name not in list(param_dict.keys()):
[b00414d]1956                return
1957            # modify the param value
1958            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1959            self._magnet_model.item(row, 1).setText(param_repr)
[c2f3ca2]1960            self.kernel_module.setParam(param_name, param_dict[param_name][0])
[b00414d]1961            if self.has_magnet_error_column:
1962                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1963                self._magnet_model.item(row, 2).setText(error_repr)
1964
1965        def createErrorColumn(row):
1966            # Utility function for error column update
1967            item = QtGui.QStandardItem()
1968            def createItem(param_name):
1969                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1970                item.setText(error_repr)
1971            def curr_param():
1972                return str(self._magnet_model.item(row, 0).text())
1973
[b3e8629]1974            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
[b00414d]1975
1976            error_column.append(item)
1977
1978        # block signals temporarily, so we don't end up
1979        # updating charts with every single model change on the end of fitting
[d2007a8]1980        self._magnet_model.dataChanged.disconnect()
[8e2cd79]1981        self.iterateOverMagnetModel(updateFittedValues)
[d2007a8]1982        self._magnet_model.dataChanged.connect(self.onMagnetModelChange)
[b00414d]1983
1984        if self.has_magnet_error_column:
1985            return
1986
1987        self.lstMagnetic.itemDelegate().addErrorColumn()
1988        error_column = []
[8e2cd79]1989        self.iterateOverMagnetModel(createErrorColumn)
[b00414d]1990
1991        # switch off reponse to model change
1992        self._magnet_model.insertColumn(2, error_column)
1993        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1994
1995        self.has_magnet_error_column = True
1996
[0268aed]1997    def onPlot(self):
1998        """
1999        Plot the current set of data
2000        """
[d48cc19]2001        # Regardless of previous state, this should now be `plot show` functionality only
2002        self.cmdPlot.setText("Show Plot")
[88e1f57]2003        # Force data recalculation so existing charts are updated
[d1e4689]2004        if not self.data_is_loaded:
2005            self.showTheoryPlot()
2006        else:
2007            self.showPlot()
[0cd98a1]2008        # This is an important processEvent.
2009        # This allows charts to be properly updated in order
2010        # of plots being applied.
2011        QtWidgets.QApplication.processEvents()
[33d5956]2012        self.recalculatePlotData() # recalc+plot theory again (2nd)
[d48cc19]2013
[9a7c81c]2014    def onSmearingOptionsUpdate(self):
2015        """
2016        React to changes in the smearing widget
2017        """
2018        self.calculateQGridForModel()
2019
[d48cc19]2020    def recalculatePlotData(self):
2021        """
2022        Generate a new dataset for model
2023        """
[180bd54]2024        if not self.data_is_loaded:
[0268aed]2025            self.createDefaultDataset()
2026        self.calculateQGridForModel()
2027
[d1e4689]2028    def showTheoryPlot(self):
2029        """
2030        Show the current theory plot in MPL
2031        """
2032        # Show the chart if ready
2033        if self.theory_item is None:
2034            self.recalculatePlotData()
[33d5956]2035        elif self.model_data:
[f9be147]2036            self._requestPlots(self.model_data.filename, self.theory_item.model())
[d1e4689]2037
[d48cc19]2038    def showPlot(self):
2039        """
2040        Show the current plot in MPL
2041        """
2042        # Show the chart if ready
[d1e4689]2043        data_to_show = self.data
[9ce69ec]2044        # Any models for this page
2045        current_index = self.all_data[self.data_index]
[f9be147]2046        item = self._requestPlots(self.data.filename, current_index.model())
2047        if item:
2048            # fit+data has not been shown - show just data
2049            self.communicate.plotRequestedSignal.emit([item, data_to_show], self.tab_id)
2050
2051    def _requestPlots(self, item_name, item_model):
2052        """
2053        Emits plotRequestedSignal for all plots found in the given model under the provided item name.
2054        """
[e5ae812]2055        fitpage_name = self.kernel_module.name
[f9be147]2056        plots = GuiUtils.plotsFromFilename(item_name, item_model)
[9ce69ec]2057        # Has the fitted data been shown?
2058        data_shown = False
[f9be147]2059        item = None
[9ce69ec]2060        for item, plot in plots.items():
2061            if fitpage_name in plot.name:
2062                data_shown = True
2063                self.communicate.plotRequestedSignal.emit([item, plot], self.tab_id)
[f9be147]2064        # return the last data item seen, if nothing was plotted; supposed to be just data)
2065        return None if data_shown else item
[d48cc19]2066
[180bd54]2067    def onOptionsUpdate(self):
[0268aed]2068        """
[180bd54]2069        Update local option values and replot
[0268aed]2070        """
[180bd54]2071        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
2072            self.options_widget.state()
[61a92d4]2073        # set Q range labels on the main tab
[34e5170]2074        self.lblMinRangeDef.setText(GuiUtils.formatNumber(self.q_range_min, high=True))
2075        self.lblMaxRangeDef.setText(GuiUtils.formatNumber(self.q_range_max, high=True))
[d48cc19]2076        self.recalculatePlotData()
[6c8fb2c]2077
[0268aed]2078    def setDefaultStructureCombo(self):
2079        """
2080        Fill in the structure factors combo box with defaults
2081        """
2082        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
2083        factors = [factor[0] for factor in structure_factor_list]
2084        factors.insert(0, STRUCTURE_DEFAULT)
2085        self.cbStructureFactor.clear()
2086        self.cbStructureFactor.addItems(sorted(factors))
2087
[4d457df]2088    def createDefaultDataset(self):
2089        """
2090        Generate default Dataset 1D/2D for the given model
2091        """
2092        # Create default datasets if no data passed
2093        if self.is2D:
[180bd54]2094            qmax = self.q_range_max/np.sqrt(2)
[4d457df]2095            qstep = self.npts
2096            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
[180bd54]2097            return
2098        elif self.log_points:
2099            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
[1bc27f1]2100            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
[180bd54]2101            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
[4d457df]2102        else:
[180bd54]2103            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
[1bc27f1]2104                                   num=self.npts, endpoint=True)
[180bd54]2105        self.logic.createDefault1dData(interval, self.tab_id)
[60af928]2106
[5236449]2107    def readCategoryInfo(self):
[60af928]2108        """
2109        Reads the categories in from file
2110        """
2111        self.master_category_dict = defaultdict(list)
2112        self.by_model_dict = defaultdict(list)
2113        self.model_enabled_dict = defaultdict(bool)
2114
[cbcdd2c]2115        categorization_file = CategoryInstaller.get_user_file()
2116        if not os.path.isfile(categorization_file):
2117            categorization_file = CategoryInstaller.get_default_file()
2118        with open(categorization_file, 'rb') as cat_file:
[60af928]2119            self.master_category_dict = json.load(cat_file)
[5236449]2120            self.regenerateModelDict()
[60af928]2121
[5236449]2122        # Load the model dict
2123        models = load_standard_models()
2124        for model in models:
2125            self.models[model.name] = model
2126
[3b3b40b]2127        self.readCustomCategoryInfo()
2128
2129    def readCustomCategoryInfo(self):
2130        """
2131        Reads the custom model category
2132        """
2133        #Looking for plugins
2134        self.plugins = list(self.custom_models.values())
2135        plugin_list = []
2136        for name, plug in self.custom_models.items():
2137            self.models[name] = plug
2138            plugin_list.append([name, True])
[2dce1d8]2139        if plugin_list:
2140            self.master_category_dict[CATEGORY_CUSTOM] = plugin_list
[3b3b40b]2141
[5236449]2142    def regenerateModelDict(self):
[60af928]2143        """
[cbcdd2c]2144        Regenerates self.by_model_dict which has each model name as the
[60af928]2145        key and the list of categories belonging to that model
2146        along with the enabled mapping
2147        """
2148        self.by_model_dict = defaultdict(list)
2149        for category in self.master_category_dict:
2150            for (model, enabled) in self.master_category_dict[category]:
2151                self.by_model_dict[model].append(category)
2152                self.model_enabled_dict[model] = enabled
2153
[86f88d1]2154    def addBackgroundToModel(self, model):
2155        """
2156        Adds background parameter with default values to the model
2157        """
[cbcdd2c]2158        assert isinstance(model, QtGui.QStandardItemModel)
[86f88d1]2159        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
[4d457df]2160        FittingUtilities.addCheckedListToModel(model, checked_list)
[2add354]2161        last_row = model.rowCount()-1
2162        model.item(last_row, 0).setEditable(False)
2163        model.item(last_row, 4).setEditable(False)
[86f88d1]2164
2165    def addScaleToModel(self, model):
2166        """
2167        Adds scale parameter with default values to the model
2168        """
[cbcdd2c]2169        assert isinstance(model, QtGui.QStandardItemModel)
[86f88d1]2170        checked_list = ['scale', '1.0', '0.0', 'inf', '']
[4d457df]2171        FittingUtilities.addCheckedListToModel(model, checked_list)
[2add354]2172        last_row = model.rowCount()-1
2173        model.item(last_row, 0).setEditable(False)
2174        model.item(last_row, 4).setEditable(False)
[86f88d1]2175
[9d266d2]2176    def addWeightingToData(self, data):
2177        """
2178        Adds weighting contribution to fitting data
[1bc27f1]2179        """
[b764ae5]2180        new_data = copy.deepcopy(data)
[e1e3e09]2181        # Send original data for weighting
[dc5ef15]2182        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
[9a7c81c]2183        if self.is2D:
[b764ae5]2184            new_data.err_data = weight
[9a7c81c]2185        else:
[b764ae5]2186            new_data.dy = weight
2187
2188        return new_data
[9d266d2]2189
[0268aed]2190    def updateQRange(self):
2191        """
2192        Updates Q Range display
2193        """
2194        if self.data_is_loaded:
2195            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
2196        # set Q range labels on the main tab
[a486918]2197        self.lblMinRangeDef.setText(GuiUtils.formatNumber(self.q_range_min, high=True))
2198        self.lblMaxRangeDef.setText(GuiUtils.formatNumber(self.q_range_max, high=True))
[0268aed]2199        # set Q range labels on the options tab
[180bd54]2200        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
[0268aed]2201
[4d457df]2202    def SASModelToQModel(self, model_name, structure_factor=None):
[60af928]2203        """
[cbcdd2c]2204        Setting model parameters into table based on selected category
[60af928]2205        """
2206        # Crete/overwrite model items
2207        self._model_model.clear()
[f3a19ad]2208        self._poly_model.clear()
2209        self._magnet_model.clear()
[5236449]2210
[f3a19ad]2211        if model_name is None:
2212            if structure_factor not in (None, "None"):
2213                # S(Q) on its own, treat the same as a form factor
2214                self.kernel_module = None
2215                self.fromStructureFactorToQModel(structure_factor)
2216            else:
2217                # No models selected
2218                return
[cd31251]2219        else:
[fd1ae6d1]2220            self.fromModelToQModel(model_name)
[70f4458]2221            self.addExtraShells()
[5236449]2222
[fd1ae6d1]2223            # Allow the SF combobox visibility for the given sasmodel
2224            self.enableStructureFactorControl(structure_factor)
[4ea8020]2225       
2226            # Add S(Q)
[605d944]2227            if self.cbStructureFactor.isEnabled():
2228                structure_factor = self.cbStructureFactor.currentText()
2229                self.fromStructureFactorToQModel(structure_factor)
[cd31251]2230
[f3a19ad]2231            # Add polydispersity to the model
[4ea8020]2232            self.poly_params = {}
[f3a19ad]2233            self.setPolyModel()
2234            # Add magnetic parameters to the model
[4ea8020]2235            self.magnet_params = {}
[f3a19ad]2236            self.setMagneticModel()
[5236449]2237
2238        # Now we claim the model has been loaded
[86f88d1]2239        self.model_is_loaded = True
[be8f4b0]2240        # Change the model name to a monicker
2241        self.kernel_module.name = self.modelName()
[9a7c81c]2242        # Update the smearing tab
2243        self.smearing_widget.updateKernelModel(kernel_model=self.kernel_module)
[86f88d1]2244
[fd1ae6d1]2245        # (Re)-create headers
2246        FittingUtilities.addHeadersToModel(self._model_model)
[6964d44]2247        self.lstParams.header().setFont(self.boldFont)
[fd1ae6d1]2248
[5236449]2249        # Update Q Ranges
2250        self.updateQRange()
2251
[fd1ae6d1]2252    def fromModelToQModel(self, model_name):
2253        """
2254        Setting model parameters into QStandardItemModel based on selected _model_
2255        """
[3b3b40b]2256        name = model_name
[e3df84e]2257        kernel_module = None
[3b3b40b]2258        if self.cbCategory.currentText() == CATEGORY_CUSTOM:
2259            # custom kernel load requires full path
2260            name = os.path.join(ModelUtilities.find_plugins_dir(), model_name+".py")
[060413c]2261        try:
2262            kernel_module = generate.load_kernel_module(name)
[e3df84e]2263        except ModuleNotFoundError as ex:
2264            pass
[4f8c17f]2265        except FileNotFoundError as ex:
2266            # can happen when name attribute not the same as actual filename
2267            pass
[e3df84e]2268
2269        if kernel_module is None:
2270            # mismatch between "name" attribute and actual filename.
2271            curr_model = self.models[model_name]
2272            name, _ = os.path.splitext(os.path.basename(curr_model.filename))
2273            try:
2274                kernel_module = generate.load_kernel_module(name)
2275            except ModuleNotFoundError as ex:
[bb477f5]2276                logger.error("Can't find the model "+ str(ex))
[e3df84e]2277                return
[dc71408]2278
2279        if hasattr(kernel_module, 'parameters'):
2280            # built-in and custom models
2281            self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
2282
2283        elif hasattr(kernel_module, 'model_info'):
2284            # for sum/multiply models
2285            self.model_parameters = kernel_module.model_info.parameters
2286
2287        elif hasattr(kernel_module, 'Model') and hasattr(kernel_module.Model, "_model_info"):
2288            # this probably won't work if there's no model_info, but just in case
2289            self.model_parameters = kernel_module.Model._model_info.parameters
2290        else:
2291            # no parameters - default to blank table
2292            msg = "No parameters found in model '{}'.".format(model_name)
2293            logger.warning(msg)
2294            self.model_parameters = modelinfo.ParameterTable([])
[fd1ae6d1]2295
2296        # Instantiate the current sasmodel
2297        self.kernel_module = self.models[model_name]()
2298
[9ce69ec]2299        # Change the model name to a monicker
2300        self.kernel_module.name = self.modelName()
2301
[fd1ae6d1]2302        # Explicitly add scale and background with default values
[6964d44]2303        temp_undo_state = self.undo_supported
2304        self.undo_supported = False
[fd1ae6d1]2305        self.addScaleToModel(self._model_model)
2306        self.addBackgroundToModel(self._model_model)
[6964d44]2307        self.undo_supported = temp_undo_state
[fd1ae6d1]2308
[0d13814]2309        self.shell_names = self.shellNamesList()
2310
[00b7ddf0]2311        # Add heading row
2312        FittingUtilities.addHeadingRowToModel(self._model_model, model_name)
[aca8418]2313
[fd1ae6d1]2314        # Update the QModel
[04f775d]2315        FittingUtilities.addParametersToModel(
2316                self.model_parameters,
2317                self.kernel_module,
[a758043]2318                self.is2D,
2319                self._model_model,
2320                self.lstParams)
[fd1ae6d1]2321
2322    def fromStructureFactorToQModel(self, structure_factor):
2323        """
2324        Setting model parameters into QStandardItemModel based on selected _structure factor_
2325        """
[605d944]2326        if structure_factor is None or structure_factor=="None":
2327            return
[18d5c94a]2328
[01b4877]2329        product_params = None
2330
[5fb714b]2331        if self.kernel_module is None:
2332            # Structure factor is the only selected model; build it and show all its params
2333            self.kernel_module = self.models[structure_factor]()
[9ce69ec]2334            self.kernel_module.name = self.modelName()
[5fb714b]2335            s_params = self.kernel_module._model_info.parameters
2336            s_params_orig = s_params
[f3a19ad]2337        else:
[5fb714b]2338            s_kernel = self.models[structure_factor]()
2339            p_kernel = self.kernel_module
[9562a6b]2340            # need to reset multiplicity to get the right product
[672c0e97]2341            if p_kernel.is_multiplicity_model:
2342                p_kernel.multiplicity = p_kernel.multiplicity_info.number
[5d1440e1]2343
[f3a19ad]2344            p_pars_len = len(p_kernel._model_info.parameters.kernel_parameters)
2345            s_pars_len = len(s_kernel._model_info.parameters.kernel_parameters)
[fd1ae6d1]2346
[f3a19ad]2347            self.kernel_module = MultiplicationModel(p_kernel, s_kernel)
[9ce69ec]2348            # Modify the name to correspond to shown items
2349            self.kernel_module.name = self.modelName()
[f3a19ad]2350            all_params = self.kernel_module._model_info.parameters.kernel_parameters
2351            all_param_names = [param.name for param in all_params]
[18d5c94a]2352
[f3a19ad]2353            # S(Q) params from the product model are not necessarily the same as those from the S(Q) model; any
[5fb714b]2354            # conflicting names with P(Q) params will cause a rename
[fd1ae6d1]2355
[f3a19ad]2356            if "radius_effective_mode" in all_param_names:
2357                # Show all parameters
[01b4877]2358                # In this case, radius_effective is NOT pruned by sasmodels.product
[f3a19ad]2359                s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len])
2360                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters)
[01b4877]2361                product_params = modelinfo.ParameterTable(
2362                        self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
[f3a19ad]2363            else:
2364                # Ensure radius_effective is not displayed
2365                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters[1:])
2366                if "radius_effective" in all_param_names:
[01b4877]2367                    # In this case, radius_effective is NOT pruned by sasmodels.product
[f3a19ad]2368                    s_params = modelinfo.ParameterTable(all_params[p_pars_len+1:p_pars_len+s_pars_len])
[01b4877]2369                    product_params = modelinfo.ParameterTable(
2370                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
[f3a19ad]2371                else:
[01b4877]2372                    # In this case, radius_effective is pruned by sasmodels.product
[f3a19ad]2373                    s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len-1])
[01b4877]2374                    product_params = modelinfo.ParameterTable(
2375                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len-1:])
[f3a19ad]2376
[00b7ddf0]2377        # Add heading row
2378        FittingUtilities.addHeadingRowToModel(self._model_model, structure_factor)
[0d72cac]2379
[b69b549]2380        # Get new rows for QModel
2381        # Any renamed parameters are stored as data in the relevant item, for later handling
[04f775d]2382        FittingUtilities.addSimpleParametersToModel(
[01b4877]2383                parameters=s_params,
2384                is2D=self.is2D,
2385                parameters_original=s_params_orig,
2386                model=self._model_model,
2387                view=self.lstParams)
2388
2389        # Insert product-only params into QModel
2390        if product_params:
2391            prod_rows = FittingUtilities.addSimpleParametersToModel(
2392                    parameters=product_params,
2393                    is2D=self.is2D,
2394                    parameters_original=None,
2395                    model=self._model_model,
2396                    view=self.lstParams,
2397                    row_num=2)
2398
2399            # Since this all happens after shells are dealt with and we've inserted rows, fix this counter
2400            self._n_shells_row += len(prod_rows)
[fd1ae6d1]2401
[6dbff18]2402    def haveParamsToFit(self):
2403        """
2404        Finds out if there are any parameters ready to be fitted
2405        """
[d4216ac]2406        if not self.logic.data_is_loaded:
2407            return False
2408        if self.main_params_to_fit:
2409            return True
2410        if self.chkPolydispersity.isChecked() and self.poly_params_to_fit:
2411            return True
[a98014ab]2412        if self.chkMagnetism.isChecked() and self.canHaveMagnetism() and self.magnet_params_to_fit:
[d4216ac]2413            return True
2414        return False
[6dbff18]2415
[d2007a8]2416    def onMainParamsChange(self, top, bottom):
[cd31251]2417        """
2418        Callback method for updating the sasmodel parameters with the GUI values
2419        """
[d2007a8]2420        item = self._model_model.itemFromIndex(top)
2421
[cbcdd2c]2422        model_column = item.column()
[cd31251]2423
2424        if model_column == 0:
[f182f93]2425            self.checkboxSelected(item)
[6dbff18]2426            self.cmdFit.setEnabled(self.haveParamsToFit())
[6964d44]2427            # Update state stack
2428            self.updateUndo()
[cd31251]2429            return
2430
[f182f93]2431        model_row = item.row()
2432        name_index = self._model_model.index(model_row, 0)
[700b03b]2433        name_item = self._model_model.itemFromIndex(name_index)
[f182f93]2434
[b00414d]2435        # Extract changed value.
[2add354]2436        try:
[fbfc488]2437            value = GuiUtils.toDouble(item.text())
[0261bc1]2438        except TypeError:
[2add354]2439            # Unparsable field
2440            return
[fbfc488]2441
[700b03b]2442        # if the item has user data, this is the actual parameter name (e.g. to handle duplicate names)
2443        if name_item.data(QtCore.Qt.UserRole):
2444            parameter_name = str(name_item.data(QtCore.Qt.UserRole))
2445        else:
2446            parameter_name = str(self._model_model.data(name_index))
[cbcdd2c]2447
[00b3b40]2448        # Update the parameter value - note: this supports +/-inf as well
[8f2548c]2449        param_column = self.lstParams.itemDelegate().param_value
2450        min_column = self.lstParams.itemDelegate().param_min
2451        max_column = self.lstParams.itemDelegate().param_max
2452        if model_column == param_column:
[d7d55c7]2453            # don't try to update multiplicity counters if they aren't there.
2454            # Note that this will fail for proper bad update where the model
2455            # doesn't contain multiplicity parameter
2456            if parameter_name != self.kernel_module.multiplicity_info.control:
2457                self.kernel_module.setParam(parameter_name, value)
[8f2548c]2458        elif model_column == min_column:
[8a32a6ff]2459            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
[8f2548c]2460            self.kernel_module.details[parameter_name][1] = value
2461        elif model_column == max_column:
2462            self.kernel_module.details[parameter_name][2] = value
2463        else:
2464            # don't update the chart
2465            return
[00b3b40]2466
2467        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
2468        # TODO: multishell params in self.kernel_module.details[??] = value
[cbcdd2c]2469
[5a96a72]2470        # handle display of effective radius parameter according to radius_effective_mode; pass ER into model if
2471        # necessary
2472        self.processEffectiveRadius()
2473
[d7ff531]2474        # Force the chart update when actual parameters changed
2475        if model_column == 1:
[d48cc19]2476            self.recalculatePlotData()
[7d077d1]2477
[2241130]2478        # Update state stack
[00b3b40]2479        self.updateUndo()
[2241130]2480
[5a96a72]2481    def processEffectiveRadius(self):
2482        """
2483        Checks the value of radius_effective_mode, if existent, and processes radius_effective as necessary.
2484        * mode == 0: This means 'unconstrained'; ensure use can specify ER.
2485        * mode > 0: This means it is constrained to a P(Q)-computed value in sasmodels; prevent user from editing ER.
2486
2487        Note: If ER has been computed, it is passed back to SasView as an intermediate result. That value must be
2488        displayed for the user; that is not dealt with here, but in complete1D.
2489        """
2490        ER_row = self.getRowFromName("radius_effective")
2491        if ER_row is None:
2492            return
2493
2494        ER_mode_row = self.getRowFromName("radius_effective_mode")
2495        if ER_mode_row is None:
2496            return
2497
2498        try:
2499            ER_mode = int(self._model_model.item(ER_mode_row, 1).text())
2500        except ValueError:
2501            logging.error("radius_effective_mode was set to an invalid value.")
2502            return
2503
2504        if ER_mode == 0:
2505            # ensure the ER value can be modified by user
2506            self.setParamEditableByRow(ER_row, True)
2507        elif ER_mode > 0:
2508            # ensure the ER value cannot be modified by user
2509            self.setParamEditableByRow(ER_row, False)
2510        else:
2511            logging.error("radius_effective_mode was set to an invalid value.")
2512
2513    def setParamEditableByRow(self, row, editable=True):
2514        """
2515        Sets whether the user can edit a parameter in the table. If they cannot, the parameter name's font is changed,
2516        the value itself cannot be edited if clicked on, and the parameter may not be fitted.
2517        """
2518        item_name = self._model_model.item(row, 0)
2519        item_value = self._model_model.item(row, 1)
2520
2521        item_value.setEditable(editable)
2522
2523        if editable:
2524            # reset font
2525            item_name.setFont(QtGui.QFont())
2526            # reset colour
2527            item_name.setForeground(QtGui.QBrush())
2528            # make checkable
2529            item_name.setCheckable(True)
2530        else:
2531            # change font
2532            font = QtGui.QFont()
2533            font.setItalic(True)
2534            item_name.setFont(font)
2535            # change colour
2536            item_name.setForeground(QtGui.QBrush(QtGui.QColor(50, 50, 50)))
2537            # make not checkable (and uncheck)
2538            item_name.setCheckState(QtCore.Qt.Unchecked)
2539            item_name.setCheckable(False)
2540
[7fd20fc]2541    def isCheckable(self, row):
2542        return self._model_model.item(row, 0).isCheckable()
2543
[442a9ae]2544    def selectCheckbox(self, row):
2545        """
2546        Select the checkbox in given row.
2547        """
2548        assert 0<= row <= self._model_model.rowCount()
2549        index = self._model_model.index(row, 0)
2550        item = self._model_model.itemFromIndex(index)
2551        item.setCheckState(QtCore.Qt.Checked)
2552
[f182f93]2553    def checkboxSelected(self, item):
2554        # Assure we're dealing with checkboxes
2555        if not item.isCheckable():
2556            return
2557        status = item.checkState()
2558
2559        # If multiple rows selected - toggle all of them, filtering uncheckable
2560        # Convert to proper indices and set requested enablement
[7fd20fc]2561        self.setParameterSelection(status)
[f182f93]2562
2563        # update the list of parameters to fit
[6dbff18]2564        self.main_params_to_fit = self.checkedListFromModel(self._model_model)
[c1e380e]2565
2566    def checkedListFromModel(self, model):
2567        """
2568        Returns list of checked parameters for given model
2569        """
2570        def isChecked(row):
2571            return model.item(row, 0).checkState() == QtCore.Qt.Checked
2572
2573        return [str(model.item(row_index, 0).text())
[b3e8629]2574                for row_index in range(model.rowCount())
[c1e380e]2575                if isChecked(row_index)]
[f182f93]2576
[6fd4e36]2577    def createNewIndex(self, fitted_data):
2578        """
2579        Create a model or theory index with passed Data1D/Data2D
2580        """
2581        if self.data_is_loaded:
[0268aed]2582            if not fitted_data.name:
2583                name = self.nameForFittedData(self.data.filename)
2584                fitted_data.title = name
2585                fitted_data.name = name
2586                fitted_data.filename = name
[7d077d1]2587                fitted_data.symbol = "Line"
[6fd4e36]2588            self.updateModelIndex(fitted_data)
2589        else:
[3ae9179]2590            if not fitted_data.name:
2591                name = self.nameForFittedData(self.kernel_module.id)
2592            else:
2593                name = fitted_data.name
[0268aed]2594            fitted_data.title = name
2595            fitted_data.filename = name
2596            fitted_data.symbol = "Line"
[6fd4e36]2597            self.createTheoryIndex(fitted_data)
[5d28d6b]2598            # Switch to the theory tab for user's glee
2599            self.communicate.changeDataExplorerTabSignal.emit(1)
[6fd4e36]2600
2601    def updateModelIndex(self, fitted_data):
2602        """
2603        Update a QStandardModelIndex containing model data
2604        """
[00b3b40]2605        name = self.nameFromData(fitted_data)
[0268aed]2606        # Make this a line if no other defined
[7d077d1]2607        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
[0268aed]2608            fitted_data.symbol = 'Line'
[6fd4e36]2609        # Notify the GUI manager so it can update the main model in DataExplorer
[d4dac80]2610        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
[6fd4e36]2611
2612    def createTheoryIndex(self, fitted_data):
2613        """
2614        Create a QStandardModelIndex containing model data
2615        """
[00b3b40]2616        name = self.nameFromData(fitted_data)
2617        # Notify the GUI manager so it can create the theory model in DataExplorer
[cb90b65]2618        self.theory_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
2619        self.communicate.updateTheoryFromPerspectiveSignal.emit(self.theory_item)
[00b3b40]2620
2621    def nameFromData(self, fitted_data):
2622        """
2623        Return name for the dataset. Terribly impure function.
2624        """
[0268aed]2625        if fitted_data.name is None:
[00b3b40]2626            name = self.nameForFittedData(self.logic.data.filename)
[0268aed]2627            fitted_data.title = name
2628            fitted_data.name = name
2629            fitted_data.filename = name
2630        else:
2631            name = fitted_data.name
[00b3b40]2632        return name
[5236449]2633
[4d457df]2634    def methodCalculateForData(self):
2635        '''return the method for data calculation'''
2636        return Calc1D if isinstance(self.data, Data1D) else Calc2D
2637
2638    def methodCompleteForData(self):
2639        '''return the method for result parsin on calc complete '''
[d4dac80]2640        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
[4d457df]2641
[66d4370]2642    def updateKernelModelWithExtraParams(self, model=None):
2643        """
2644        Updates kernel model 'model' with extra parameters from
2645        the polydisp and magnetism tab, if the tabs are enabled
2646        """
2647        if model is None: return
2648        if not hasattr(model, 'setParam'): return
2649
2650        # add polydisperse parameters if asked
[2d47985]2651        if self.chkPolydispersity.isChecked() and self._poly_model.rowCount() > 0:
[66d4370]2652            for key, value in self.poly_params.items():
2653                model.setParam(key, value)
2654        # add magnetic params if asked
[a98014ab]2655        if self.chkMagnetism.isChecked() and self.canHaveMagnetism() and self._magnet_model.rowCount() > 0:
[91f4884]2656            for key, value in self.magnet_params.items():
[66d4370]2657                model.setParam(key, value)
2658
[d4dac80]2659    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
[86f88d1]2660        """
[d4dac80]2661        Wrapper for Calc1D/2D calls
[86f88d1]2662        """
[d4dac80]2663        if data is None:
2664            data = self.data
2665        if model is None:
[66d4370]2666            model = copy.deepcopy(self.kernel_module)
2667            self.updateKernelModelWithExtraParams(model)
2668
[d4dac80]2669        if completefn is None:
2670            completefn = self.methodCompleteForData()
[9a7c81c]2671        smearer = self.smearing_widget.smearer()
[b764ae5]2672        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2673
[557fc498]2674        # Disable buttons/table
[a2b8607]2675        self.disableInteractiveElementsOnCalculate()
[4d457df]2676        # Awful API to a backend method.
[d4dac80]2677        calc_thread = self.methodCalculateForData()(data=data,
2678                                               model=model,
[1bc27f1]2679                                               page_id=0,
2680                                               qmin=self.q_range_min,
2681                                               qmax=self.q_range_max,
[9a7c81c]2682                                               smearer=smearer,
[1bc27f1]2683                                               state=None,
[b764ae5]2684                                               weight=weight,
[1bc27f1]2685                                               fid=None,
2686                                               toggle_mode_on=False,
[d4dac80]2687                                               completefn=completefn,
[1bc27f1]2688                                               update_chisqr=True,
2689                                               exception_handler=self.calcException,
2690                                               source=None)
[d4dac80]2691        if use_threads:
2692            if LocalConfig.USING_TWISTED:
2693                # start the thread with twisted
2694                thread = threads.deferToThread(calc_thread.compute)
2695                thread.addCallback(completefn)
2696                thread.addErrback(self.calculateDataFailed)
2697            else:
2698                # Use the old python threads + Queue
2699                calc_thread.queue()
2700                calc_thread.ready(2.5)
2701        else:
2702            results = calc_thread.compute()
2703            completefn(results)
[4d457df]2704
[d4dac80]2705    def calculateQGridForModel(self):
2706        """
2707        Prepare the fitting data object, based on current ModelModel
2708        """
2709        if self.kernel_module is None:
2710            return
2711        self.calculateQGridForModelExt()
[6964d44]2712
[aca8418]2713    def calculateDataFailed(self, reason):
[6964d44]2714        """
[c1e380e]2715        Thread returned error
[6964d44]2716        """
[557fc498]2717        # Bring the GUI to normal state
2718        self.enableInteractiveElements()
[b3e8629]2719        print("Calculate Data failed with ", reason)
[5236449]2720
[d4dac80]2721    def completed1D(self, return_data):
2722        self.Calc1DFinishedSignal.emit(return_data)
2723
2724    def completed2D(self, return_data):
2725        self.Calc2DFinishedSignal.emit(return_data)
2726
[cbcdd2c]2727    def complete1D(self, return_data):
[5236449]2728        """
[4d457df]2729        Plot the current 1D data
2730        """
[557fc498]2731        # Bring the GUI to normal state
2732        self.enableInteractiveElements()
[9ce69ec]2733        if return_data is None:
2734            return
[d48cc19]2735        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
[d1e4689]2736
[712db9e]2737        # assure the current index is set properly for batch
2738        if len(self._logic) > 1:
2739            for i, logic in enumerate(self._logic):
2740                if logic.data.name in fitted_data.name:
2741                    self.data_index = i
2742
[0fe7e5b]2743        residuals = self.calculateResiduals(fitted_data)
[d48cc19]2744        self.model_data = fitted_data
[5aad7a5]2745        new_plots = [fitted_data]
2746        if residuals is not None:
2747            new_plots.append(residuals)
[cbcdd2c]2748
[fd7ef36]2749        if self.data_is_loaded:
[3ae70f9]2750            # delete any plots associated with the data that were not updated
2751            # (e.g. to remove beta(Q), S_eff(Q))
[0dcb71d]2752            GuiUtils.deleteRedundantPlots(self.all_data[self.data_index], new_plots)
[5b144c6]2753            pass
[fd7ef36]2754        else:
[3ae70f9]2755            # delete theory items for the model, in order to get rid of any
2756            # redundant items, e.g. beta(Q), S_eff(Q)
[fd7ef36]2757            self.communicate.deleteIntermediateTheoryPlotsSignal.emit(self.kernel_module.id)
[0fe7e5b]2758
[d8d81ea]2759        # Create plots for parameters with enabled polydispersity
2760        for plot in FittingUtilities.plotPolydispersities(return_data.get('model', None)):
2761            data_id = fitted_data.id.split()
2762            plot.id = "{} [{}] {}".format(data_id[0], plot.name, " ".join(data_id[1:]))
2763            data_name = fitted_data.name.split()
2764            plot.name = " ".join([data_name[0], plot.name] + data_name[1:])
2765            self.createNewIndex(plot)
2766            new_plots.append(plot)
2767
[3ae9179]2768        # Create plots for intermediate product data
[40975f8]2769        plots = self.logic.new1DProductPlots(return_data, self.tab_id)
2770        for plot in plots:
2771            plot.symbol = "Line"
2772            self.createNewIndex(plot)
2773            new_plots.append(plot)
[0fe7e5b]2774
[9463ca2]2775        for plot in new_plots:
[5aad7a5]2776            self.communicate.plotUpdateSignal.emit([plot])
[9463ca2]2777
[5a96a72]2778        # Update radius_effective if relevant
[d99b1a5]2779        self.updateEffectiveRadius(return_data)
[5a96a72]2780
[cbcdd2c]2781    def complete2D(self, return_data):
2782        """
[4d457df]2783        Plot the current 2D data
2784        """
[557fc498]2785        # Bring the GUI to normal state
2786        self.enableInteractiveElements()
2787
[8a09457]2788        if return_data is None:
2789            return
2790
[6fd4e36]2791        fitted_data = self.logic.new2DPlot(return_data)
[8a09457]2792        # assure the current index is set properly for batch
2793        if len(self._logic) > 1:
2794            for i, logic in enumerate(self._logic):
2795                if logic.data.name in fitted_data.name:
2796                    self.data_index = i
2797
[66d4370]2798        residuals = self.calculateResiduals(fitted_data)
[d48cc19]2799        self.model_data = fitted_data
[66d4370]2800        new_plots = [fitted_data]
2801        if residuals is not None:
2802            new_plots.append(residuals)
2803
2804        # Update/generate plots
2805        for plot in new_plots:
2806            self.communicate.plotUpdateSignal.emit([plot])
[6fd4e36]2807
[d99b1a5]2808    def updateEffectiveRadius(self, return_data):
[80a327d]2809        """
2810        Given return data from sasmodels, update the effective radius parameter in the GUI table with the new
2811        calculated value as returned by sasmodels (if the value was returned).
2812        """
2813        ER_mode_row = self.getRowFromName("radius_effective_mode")
2814        if ER_mode_row is None:
2815            return
2816        try:
2817            ER_mode = int(self._model_model.item(ER_mode_row, 1).text())
2818        except ValueError:
2819            logging.error("radius_effective_mode was set to an invalid value.")
2820            return
2821        if ER_mode < 1:
2822            # does not need updating if it is not being computed
2823            return
2824
2825        ER_row = self.getRowFromName("radius_effective")
2826        if ER_row is None:
2827            return
2828
2829        scalar_results = self.logic.getScalarIntermediateResults(return_data)
2830        ER_value = scalar_results.get("effective_radius") # note name of key
2831        if ER_value is None:
2832            return
2833        # ensure the model does not recompute when updating the value
2834        self._model_model.blockSignals(True)
2835        self._model_model.item(ER_row, 1).setText(str(ER_value))
2836        self._model_model.blockSignals(False)
2837        # ensure the view is updated immediately
2838        self._model_model.layoutChanged.emit()
2839
[6fd4e36]2840    def calculateResiduals(self, fitted_data):
2841        """
[9463ca2]2842        Calculate and print Chi2 and display chart of residuals. Returns residuals plot object.
[6fd4e36]2843        """
2844        # Create a new index for holding data
[7d077d1]2845        fitted_data.symbol = "Line"
[6964d44]2846
2847        # Modify fitted_data with weighting
[b764ae5]2848        weighted_data = self.addWeightingToData(fitted_data)
[6964d44]2849
[b764ae5]2850        self.createNewIndex(weighted_data)
[712db9e]2851
[6fd4e36]2852        # Calculate difference between return_data and logic.data
[712db9e]2853        self.chi2 = FittingUtilities.calculateChi2(weighted_data, self.data)
[6fd4e36]2854        # Update the control
[2add354]2855        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
[f182f93]2856        self.lblChi2Value.setText(chi2_repr)
[cbcdd2c]2857
[0268aed]2858        # Plot residuals if actual data
[aca8418]2859        if not self.data_is_loaded:
2860            return
2861
[b764ae5]2862        residuals_plot = FittingUtilities.plotResiduals(self.data, weighted_data)
[712db9e]2863        if residuals_plot is None:
2864            return
[aca8418]2865        residuals_plot.id = "Residual " + residuals_plot.id
[a54bbf2b]2866        residuals_plot.plot_role = Data1D.ROLE_RESIDUAL
[aca8418]2867        self.createNewIndex(residuals_plot)
[0fe7e5b]2868        return residuals_plot
[5236449]2869
[3d18691]2870    def onCategoriesChanged(self):
2871            """
2872            Reload the category/model comboboxes
2873            """
2874            # Store the current combo indices
2875            current_cat = self.cbCategory.currentText()
2876            current_model = self.cbModel.currentText()
2877
2878            # reread the category file and repopulate the combo
2879            self.cbCategory.blockSignals(True)
2880            self.cbCategory.clear()
2881            self.readCategoryInfo()
2882            self.initializeCategoryCombo()
2883
2884            # Scroll back to the original index in Categories
2885            new_index = self.cbCategory.findText(current_cat)
2886            if new_index != -1:
2887                self.cbCategory.setCurrentIndex(new_index)
2888            self.cbCategory.blockSignals(False)
2889            # ...and in the Models
2890            self.cbModel.blockSignals(True)
2891            new_index = self.cbModel.findText(current_model)
2892            if new_index != -1:
2893                self.cbModel.setCurrentIndex(new_index)
2894            self.cbModel.blockSignals(False)
2895
2896            return
2897
[5236449]2898    def calcException(self, etype, value, tb):
2899        """
[c1e380e]2900        Thread threw an exception.
[5236449]2901        """
[557fc498]2902        # Bring the GUI to normal state
2903        self.enableInteractiveElements()
[c1e380e]2904        # TODO: remimplement thread cancellation
[bb477f5]2905        logger.error("".join(traceback.format_exception(etype, value, tb)))
[60af928]2906
2907    def setTableProperties(self, table):
2908        """
2909        Setting table properties
2910        """
2911        # Table properties
2912        table.verticalHeader().setVisible(False)
2913        table.setAlternatingRowColors(True)
[4992ff2]2914        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2915        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
[f46f6dc]2916        table.resizeColumnsToContents()
2917
[60af928]2918        # Header
2919        header = table.horizontalHeader()
[4992ff2]2920        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2921        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
[f46f6dc]2922
[4992ff2]2923        # Qt5: the following 2 lines crash - figure out why!
[e43fc91]2924        # Resize column 0 and 7 to content
[4992ff2]2925        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2926        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
[60af928]2927
2928    def setPolyModel(self):
2929        """
2930        Set polydispersity values
2931        """
[86f88d1]2932        if not self.model_parameters:
2933            return
2934        self._poly_model.clear()
2935
[f53bc04]2936        parameters = self.model_parameters.form_volume_parameters
2937        if self.is2D:
2938            parameters += self.model_parameters.orientation_parameters
2939
[e43fc91]2940        [self.setPolyModelParameters(i, param) for i, param in \
[f53bc04]2941            enumerate(parameters) if param.polydisperse]
2942
[4d457df]2943        FittingUtilities.addPolyHeadersToModel(self._poly_model)
[60af928]2944
[e43fc91]2945    def setPolyModelParameters(self, i, param):
[aca8418]2946        """
[0d13814]2947        Standard of multishell poly parameter driver
[aca8418]2948        """
[0d13814]2949        param_name = param.name
2950        # see it the parameter is multishell
[06b0138]2951        if '[' in param.name:
[0d13814]2952            # Skip empty shells
2953            if self.current_shell_displayed == 0:
2954                return
2955            else:
2956                # Create as many entries as current shells
[b3e8629]2957                for ishell in range(1, self.current_shell_displayed+1):
[0d13814]2958                    # Remove [n] and add the shell numeral
2959                    name = param_name[0:param_name.index('[')] + str(ishell)
[e43fc91]2960                    self.addNameToPolyModel(i, name)
[0d13814]2961        else:
2962            # Just create a simple param entry
[e43fc91]2963            self.addNameToPolyModel(i, param_name)
[0d13814]2964
[e43fc91]2965    def addNameToPolyModel(self, i, param_name):
[0d13814]2966        """
2967        Creates a checked row in the poly model with param_name
2968        """
[144ec831]2969        # Polydisp. values from the sasmodel
[0d13814]2970        width = self.kernel_module.getParam(param_name + '.width')
2971        npts = self.kernel_module.getParam(param_name + '.npts')
2972        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2973        _, min, max = self.kernel_module.details[param_name]
[144ec831]2974
[66d4370]2975        # Update local param dict
2976        self.poly_params[param_name + '.width'] = width
2977        self.poly_params[param_name + '.npts'] = npts
2978        self.poly_params[param_name + '.nsigmas'] = nsigs
2979
[144ec831]2980        # Construct a row with polydisp. related variable.
2981        # This will get added to the polydisp. model
2982        # Note: last argument needs extra space padding for decent display of the control
[0d13814]2983        checked_list = ["Distribution of " + param_name, str(width),
2984                        str(min), str(max),
[e43fc91]2985                        str(npts), str(nsigs), "gaussian      ",'']
[aca8418]2986        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2987
2988        # All possible polydisp. functions as strings in combobox
[4992ff2]2989        func = QtWidgets.QComboBox()
[b3e8629]2990        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
[e43fc91]2991        # Set the default index
[aca8418]2992        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
[e43fc91]2993        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2994        self.lstPoly.setIndexWidget(ind, func)
2995        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2996
2997    def onPolyFilenameChange(self, row_index):
2998        """
2999        Respond to filename_updated signal from the delegate
3000        """
3001        # For the given row, invoke the "array" combo handler
3002        array_caption = 'array'
[8222f171]3003
[e43fc91]3004        # Get the combo box reference
3005        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
3006        widget = self.lstPoly.indexWidget(ind)
[8222f171]3007
[e43fc91]3008        # Update the combo box so it displays "array"
3009        widget.blockSignals(True)
3010        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
3011        widget.blockSignals(False)
[aca8418]3012
[8222f171]3013        # Invoke the file reader
3014        self.onPolyComboIndexChange(array_caption, row_index)
3015
[aca8418]3016    def onPolyComboIndexChange(self, combo_string, row_index):
3017        """
3018        Modify polydisp. defaults on function choice
3019        """
[144ec831]3020        # Get npts/nsigs for current selection
[aca8418]3021        param = self.model_parameters.form_volume_parameters[row_index]
[e43fc91]3022        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
3023        combo_box = self.lstPoly.indexWidget(file_index)
[aca8418]3024
[919d47c]3025        def updateFunctionCaption(row):
3026            # Utility function for update of polydispersity function name in the main model
[00b7ddf0]3027            if not self.isCheckable(row):
3028                return
[73665a8]3029            self._model_model.blockSignals(True)
[1643d8ed]3030            param_name = str(self._model_model.item(row, 0).text())
[73665a8]3031            self._model_model.blockSignals(False)
[919d47c]3032            if param_name !=  param.name:
3033                return
[144ec831]3034            # Modify the param value
[73665a8]3035            self._model_model.blockSignals(True)
[906e0c7]3036            if self.has_error_column:
3037                # err column changes the indexing
3038                self._model_model.item(row, 0).child(0).child(0,5).setText(combo_string)
3039            else:
3040                self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
[73665a8]3041            self._model_model.blockSignals(False)
[919d47c]3042
[aca8418]3043        if combo_string == 'array':
3044            try:
[e43fc91]3045                self.loadPolydispArray(row_index)
[919d47c]3046                # Update main model for display
3047                self.iterateOverModel(updateFunctionCaption)
[e43fc91]3048                # disable the row
3049                lo = self.lstPoly.itemDelegate().poly_pd
3050                hi = self.lstPoly.itemDelegate().poly_function
[b3e8629]3051                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
[aca8418]3052                return
[e43fc91]3053            except IOError:
[8222f171]3054                combo_box.setCurrentIndex(self.orig_poly_index)
[e43fc91]3055                # Pass for cancel/bad read
3056                pass
[aca8418]3057
3058        # Enable the row in case it was disabled by Array
[919d47c]3059        self._poly_model.blockSignals(True)
[e43fc91]3060        max_range = self.lstPoly.itemDelegate().poly_filename
[b3e8629]3061        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
[e43fc91]3062        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
[b3e8629]3063        self._poly_model.setData(file_index, "")
[919d47c]3064        self._poly_model.blockSignals(False)
[aca8418]3065
[8eaa101]3066        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
3067        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
[aca8418]3068
3069        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
3070        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
3071
[b3e8629]3072        self._poly_model.setData(npts_index, npts)
3073        self._poly_model.setData(nsigs_index, nsigs)
[aca8418]3074
[919d47c]3075        self.iterateOverModel(updateFunctionCaption)
[8222f171]3076        self.orig_poly_index = combo_box.currentIndex()
[919d47c]3077
[e43fc91]3078    def loadPolydispArray(self, row_index):
[aca8418]3079        """
3080        Show the load file dialog and loads requested data into state
3081        """
[4992ff2]3082        datafile = QtWidgets.QFileDialog.getOpenFileName(
3083            self, "Choose a weight file", "", "All files (*.*)", None,
[fbfc488]3084            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
[72f4834]3085
[fbfc488]3086        if not datafile:
[bb477f5]3087            logger.info("No weight data chosen.")
[1643d8ed]3088            raise IOError
[72f4834]3089
[aca8418]3090        values = []
3091        weights = []
[919d47c]3092        def appendData(data_tuple):
3093            """
3094            Fish out floats from a tuple of strings
3095            """
3096            try:
3097                values.append(float(data_tuple[0]))
3098                weights.append(float(data_tuple[1]))
3099            except (ValueError, IndexError):
3100                # just pass through if line with bad data
3101                return
3102
[aca8418]3103        with open(datafile, 'r') as column_file:
3104            column_data = [line.rstrip().split() for line in column_file.readlines()]
[919d47c]3105            [appendData(line) for line in column_data]
[aca8418]3106
[1643d8ed]3107        # If everything went well - update the sasmodel values
[aca8418]3108        self.disp_model = POLYDISPERSITY_MODELS['array']()
3109        self.disp_model.set_weights(np.array(values), np.array(weights))
[e43fc91]3110        # + update the cell with filename
3111        fname = os.path.basename(str(datafile))
3112        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
[b3e8629]3113        self._poly_model.setData(fname_index, fname)
[aca8418]3114
[f37cab0]3115    def onColumnWidthUpdate(self, index, old_size, new_size):
3116        """
3117        Simple state update of the current column widths in the  param list
3118        """
3119        self.lstParamHeaderSizes[index] = new_size
3120
[60af928]3121    def setMagneticModel(self):
3122        """
3123        Set magnetism values on model
3124        """
[86f88d1]3125        if not self.model_parameters:
3126            return
3127        self._magnet_model.clear()
[a98014ab]3128        # default initial value
[658795d]3129        m0 = 0.5
[a98014ab]3130        for param in self.model_parameters.call_parameters:
3131            if param.type != 'magnetic': continue
3132            if "M0" in param.name:
[658795d]3133                m0 += 0.5
[a98014ab]3134                value = m0
3135            else:
3136                value = param.default
3137            self.addCheckedMagneticListToModel(param, value)
3138
[4d457df]3139        FittingUtilities.addHeadersToModel(self._magnet_model)
[60af928]3140
[0d13814]3141    def shellNamesList(self):
3142        """
3143        Returns list of names of all multi-shell parameters
3144        E.g. for sld[n], radius[n], n=1..3 it will return
3145        [sld1, sld2, sld3, radius1, radius2, radius3]
3146        """
3147        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
3148        top_index = self.kernel_module.multiplicity_info.number
3149        shell_names = []
[b3e8629]3150        for i in range(1, top_index+1):
[0d13814]3151            for name in multi_names:
3152                shell_names.append(name+str(i))
3153        return shell_names
3154
[a98014ab]3155    def addCheckedMagneticListToModel(self, param, value):
[aca8418]3156        """
3157        Wrapper for model update with a subset of magnetic parameters
3158        """
[8ac8717]3159        try:
3160            basename, _ = param.name.rsplit('_', 1)
3161        except ValueError:
3162            basename = param.name
3163        if basename in self.shell_names:
[0d13814]3164            try:
[8ac8717]3165                shell_index = int(basename[-2:])
[0d13814]3166            except ValueError:
[8ac8717]3167                shell_index = int(basename[-1:])
[0d13814]3168
3169            if shell_index > self.current_shell_displayed:
3170                return
3171
[aca8418]3172        checked_list = [param.name,
[a98014ab]3173                        str(value),
[aca8418]3174                        str(param.limits[0]),
3175                        str(param.limits[1]),
3176                        param.units]
3177
[a98014ab]3178        self.magnet_params[param.name] = value
[66d4370]3179
[a98014ab]3180        FittingUtilities.addCheckedListToModel(self._magnet_model, checked_list)
[aca8418]3181
[fd1ae6d1]3182    def enableStructureFactorControl(self, structure_factor):
[cd31251]3183        """
3184        Add structure factors to the list of parameters
3185        """
[fd1ae6d1]3186        if self.kernel_module.is_form_factor or structure_factor == 'None':
[cd31251]3187            self.enableStructureCombo()
3188        else:
3189            self.disableStructureCombo()
3190
[60af928]3191    def addExtraShells(self):
3192        """
[f46f6dc]3193        Add a combobox for multiple shell display
[60af928]3194        """
[4d457df]3195        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
[f46f6dc]3196
3197        if param_length == 0:
3198            return
3199
[6f7f652]3200        # cell 1: variable name
[f46f6dc]3201        item1 = QtGui.QStandardItem(param_name)
3202
[4992ff2]3203        func = QtWidgets.QComboBox()
[60af928]3204
[6f7f652]3205        # cell 2: combobox
[f46f6dc]3206        item2 = QtGui.QStandardItem()
[13dd7d2]3207
3208        # cell 3: min value
3209        item3 = QtGui.QStandardItem()
[7f41584]3210        # set the cell to be non-editable
3211        item3.setFlags(item3.flags() ^ QtCore.Qt.ItemIsEditable)
[13dd7d2]3212
3213        # cell 4: max value
3214        item4 = QtGui.QStandardItem()
[7f41584]3215        # set the cell to be non-editable
3216        item4.setFlags(item4.flags() ^ QtCore.Qt.ItemIsEditable)
[13dd7d2]3217
[86d3207]3218        # cell 4: SLD button
3219        item5 = QtGui.QStandardItem()
3220        button = QtWidgets.QPushButton()
3221        button.setText("Show SLD Profile")
3222
3223        self._model_model.appendRow([item1, item2, item3, item4, item5])
[60af928]3224
[6f7f652]3225        # Beautify the row:  span columns 2-4
[60af928]3226        shell_row = self._model_model.rowCount()
[f46f6dc]3227        shell_index = self._model_model.index(shell_row-1, 1)
[86d3207]3228        button_index = self._model_model.index(shell_row-1, 4)
[86f88d1]3229
[4d457df]3230        self.lstParams.setIndexWidget(shell_index, func)
[86d3207]3231        self.lstParams.setIndexWidget(button_index, button)
[70f4458]3232        self._n_shells_row = shell_row - 1
[86f88d1]3233
[f712bf30]3234        # Get the default number of shells for the model
3235        kernel_pars = self.kernel_module._model_info.parameters.kernel_parameters
3236        shell_par = None
3237        for par in kernel_pars:
[7fb68060]3238            parname = par.name
3239            if '[' in parname:
3240                 parname = parname[:parname.index('[')]
3241            if parname == param_name:
[f712bf30]3242                shell_par = par
3243                break
[7fb68060]3244        if shell_par is None:
[f712bf30]3245            logger.error("Could not find %s in kernel parameters.", param_name)
[7fb68060]3246            return
[f712bf30]3247        default_shell_count = shell_par.default
[13dd7d2]3248        shell_min = 0
3249        shell_max = 0
3250        try:
3251            shell_min = int(shell_par.limits[0])
3252            shell_max = int(shell_par.limits[1])
3253        except IndexError as ex:
3254            # no info about limits
3255            pass
[7f41584]3256        except OverflowError:
3257            # Try to limit shell_par, if possible
3258            if float(shell_par.limits[1])==np.inf:
3259                shell_max = 9
3260            logging.warning("Limiting shell count to 9.")
[7fb68060]3261        except Exception as ex:
3262            logging.error("Badly defined multiplicity: "+ str(ex))
3263            return
[62c6dc0]3264        # don't update the kernel here - this data is display only
3265        self._model_model.blockSignals(True)
[13dd7d2]3266        item3.setText(str(shell_min))
3267        item4.setText(str(shell_max))
[62c6dc0]3268        self._model_model.blockSignals(False)
[13dd7d2]3269
[7fb68060]3270        ## Respond to index change
3271        #func.currentTextChanged.connect(self.modifyShellsInList)
[13dd7d2]3272
[86d3207]3273        # Respond to button press
3274        button.clicked.connect(self.onShowSLDProfile)
3275
[13dd7d2]3276        # Available range of shells displayed in the combobox
3277        func.addItems([str(i) for i in range(shell_min, shell_max+1)])
[f712bf30]3278
[7fb68060]3279        # Respond to index change
3280        func.currentTextChanged.connect(self.modifyShellsInList)
3281
[f712bf30]3282        # Add default number of shells to the model
[13dd7d2]3283        func.setCurrentText(str(default_shell_count))
[7fb68060]3284        self.modifyShellsInList(str(default_shell_count))
[a9b568c]3285
[13dd7d2]3286    def modifyShellsInList(self, text):
[86f88d1]3287        """
3288        Add/remove additional multishell parameters
3289        """
3290        # Find row location of the combobox
[70f4458]3291        first_row = self._n_shells_row + 1
3292        remove_rows = self._num_shell_params
[13dd7d2]3293        try:
3294            index = int(text)
3295        except ValueError:
3296            # bad text on the control!
3297            index = 0
3298            logger.error("Multiplicity incorrect! Setting to 0")
[86d3207]3299        self.kernel_module.multiplicity = index
[86f88d1]3300        if remove_rows > 1:
[70f4458]3301            self._model_model.removeRows(first_row, remove_rows)
3302
[b69b549]3303        new_rows = FittingUtilities.addShellsToModel(
[88ada06]3304                self.model_parameters,
3305                self._model_model,
[a758043]3306                index,
[b69b549]3307                first_row,
[a758043]3308                self.lstParams)
[86f88d1]3309
[b69b549]3310        self._num_shell_params = len(new_rows)
[a9b568c]3311        self.current_shell_displayed = index
[60af928]3312
[daf7c9c]3313        # Param values for existing shells were reset to default; force all changes into kernel module
3314        for row in new_rows:
3315            par = row[0].text()
3316            val = GuiUtils.toDouble(row[1].text())
3317            self.kernel_module.setParam(par, val)
3318
3319        # Change 'n' in the parameter model; also causes recalculation
[3090270]3320        self._model_model.item(self._n_shells_row, 1).setText(str(index))
3321
[0d13814]3322        # Update relevant models
3323        self.setPolyModel()
[d2007a8]3324        if self.canHaveMagnetism():
3325            self.setMagneticModel()
[0d13814]3326
[86d3207]3327    def onShowSLDProfile(self):
3328        """
3329        Show a quick plot of SLD profile
3330        """
3331        # get profile data
[7fb68060]3332        try:
3333            x, y = self.kernel_module.getProfile()
3334        except TypeError:
3335            msg = "SLD profile calculation failed."
3336            logging.error(msg)
3337            return
3338
[86d3207]3339        y *= 1.0e6
3340        profile_data = Data1D(x=x, y=y)
3341        profile_data.name = "SLD"
3342        profile_data.scale = 'linear'
3343        profile_data.symbol = 'Line'
3344        profile_data.hide_error = True
3345        profile_data._xaxis = "R(\AA)"
3346        profile_data._yaxis = "SLD(10^{-6}\AA^{-2})"
3347
3348        plotter = PlotterWidget(self, quickplot=True)
3349        plotter.data = profile_data
3350        plotter.showLegend = True
3351        plotter.plot(hide_error=True, marker='-')
3352
3353        self.plot_widget = QtWidgets.QWidget()
3354        self.plot_widget.setWindowTitle("Scattering Length Density Profile")
3355        layout = QtWidgets.QVBoxLayout()
3356        layout.addWidget(plotter)
3357        self.plot_widget.setLayout(layout)
3358        self.plot_widget.show()
3359
[557fc498]3360    def setInteractiveElements(self, enabled=True):
[14ec91c5]3361        """
[557fc498]3362        Switch interactive GUI elements on/off
[14ec91c5]3363        """
[557fc498]3364        assert isinstance(enabled, bool)
3365
3366        self.lstParams.setEnabled(enabled)
3367        self.lstPoly.setEnabled(enabled)
3368        self.lstMagnetic.setEnabled(enabled)
3369
3370        self.cbCategory.setEnabled(enabled)
[7fb68060]3371
3372        if enabled:
3373            # worry about original enablement of model and SF
3374            self.cbModel.setEnabled(self.enabled_cbmodel)
3375            self.cbStructureFactor.setEnabled(self.enabled_sfmodel)
3376        else:
3377            self.cbModel.setEnabled(enabled)
3378            self.cbStructureFactor.setEnabled(enabled)
3379
[3ab2105]3380        self.cmdPlot.setEnabled(enabled)
[14ec91c5]3381
[557fc498]3382    def enableInteractiveElements(self):
[14ec91c5]3383        """
[557fc498]3384        Set buttion caption on fitting/calculate finish
3385        Enable the param table(s)
[14ec91c5]3386        """
[ded5e77]3387        # Notify the user that fitting is available
3388        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
[14ec91c5]3389        self.cmdFit.setText("Fit")
[ded5e77]3390        self.fit_started = False
[557fc498]3391        self.setInteractiveElements(True)
3392
3393    def disableInteractiveElements(self):
3394        """
3395        Set buttion caption on fitting/calculate start
3396        Disable the param table(s)
3397        """
3398        # Notify the user that fitting is being run
3399        # Allow for stopping the job
3400        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
3401        self.cmdFit.setText('Stop fit')
3402        self.setInteractiveElements(False)
[14ec91c5]3403
[a2b8607]3404    def disableInteractiveElementsOnCalculate(self):
3405        """
3406        Set buttion caption on fitting/calculate start
3407        Disable the param table(s)
3408        """
3409        # Notify the user that fitting is being run
3410        # Allow for stopping the job
3411        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
3412        self.cmdFit.setText('Running...')
3413        self.setInteractiveElements(False)
3414
[672b8ab]3415    def readFitPage(self, fp):
3416        """
3417        Read in state from a fitpage object and update GUI
3418        """
3419        assert isinstance(fp, FitPage)
3420        # Main tab info
3421        self.logic.data.filename = fp.filename
3422        self.data_is_loaded = fp.data_is_loaded
3423        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
3424        self.chkMagnetism.setCheckState(fp.is_magnetic)
3425        self.chk2DView.setCheckState(fp.is2D)
3426
3427        # Update the comboboxes
3428        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
3429        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
3430        if fp.current_factor:
3431            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
3432
3433        self.chi2 = fp.chi2
3434
3435        # Options tab
3436        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
3437        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
3438        self.npts = fp.fit_options[fp.NPTS]
3439        self.log_points = fp.fit_options[fp.LOG_POINTS]
3440        self.weighting = fp.fit_options[fp.WEIGHTING]
3441
3442        # Models
[d60da0c]3443        self._model_model = fp.model_model
3444        self._poly_model = fp.poly_model
3445        self._magnet_model = fp.magnetism_model
[672b8ab]3446
3447        # Resolution tab
3448        smearing = fp.smearing_options[fp.SMEARING_OPTION]
3449        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
3450        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
3451        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
3452        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
3453
3454        # TODO: add polidyspersity and magnetism
3455
3456    def saveToFitPage(self, fp):
3457        """
3458        Write current state to the given fitpage
3459        """
3460        assert isinstance(fp, FitPage)
3461
3462        # Main tab info
3463        fp.filename = self.logic.data.filename
3464        fp.data_is_loaded = self.data_is_loaded
3465        fp.is_polydisperse = self.chkPolydispersity.isChecked()
3466        fp.is_magnetic = self.chkMagnetism.isChecked()
3467        fp.is2D = self.chk2DView.isChecked()
3468        fp.data = self.data
3469
3470        # Use current models - they contain all the required parameters
3471        fp.model_model = self._model_model
3472        fp.poly_model = self._poly_model
3473        fp.magnetism_model = self._magnet_model
3474
3475        if self.cbCategory.currentIndex() != 0:
3476            fp.current_category = str(self.cbCategory.currentText())
3477            fp.current_model = str(self.cbModel.currentText())
3478
3479        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
3480            fp.current_factor = str(self.cbStructureFactor.currentText())
3481        else:
3482            fp.current_factor = ''
3483
3484        fp.chi2 = self.chi2
[6dbff18]3485        fp.main_params_to_fit = self.main_params_to_fit
3486        fp.poly_params_to_fit = self.poly_params_to_fit
3487        fp.magnet_params_to_fit = self.magnet_params_to_fit
[6964d44]3488        fp.kernel_module = self.kernel_module
[672b8ab]3489
[6ff2eb3]3490        # Algorithm options
3491        # fp.algorithm = self.parent.fit_options.selected_id
3492
[672b8ab]3493        # Options tab
3494        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
3495        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
3496        fp.fit_options[fp.NPTS] = self.npts
3497        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
3498        fp.fit_options[fp.LOG_POINTS] = self.log_points
3499        fp.fit_options[fp.WEIGHTING] = self.weighting
3500
3501        # Resolution tab
3502        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3503        fp.smearing_options[fp.SMEARING_OPTION] = smearing
3504        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
3505        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
3506        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
3507
3508        # TODO: add polidyspersity and magnetism
3509
[00b3b40]3510    def updateUndo(self):
3511        """
3512        Create a new state page and add it to the stack
3513        """
3514        if self.undo_supported:
3515            self.pushFitPage(self.currentState())
3516
[672b8ab]3517    def currentState(self):
3518        """
3519        Return fit page with current state
3520        """
3521        new_page = FitPage()
3522        self.saveToFitPage(new_page)
3523
3524        return new_page
3525
3526    def pushFitPage(self, new_page):
3527        """
3528        Add a new fit page object with current state
3529        """
[6011788]3530        self.page_stack.append(new_page)
[672b8ab]3531
3532    def popFitPage(self):
3533        """
3534        Remove top fit page from stack
3535        """
[6011788]3536        if self.page_stack:
3537            self.page_stack.pop()
[672b8ab]3538
[57be490]3539    def getReport(self):
3540        """
3541        Create and return HTML report with parameters and charts
3542        """
3543        index = None
3544        if self.all_data:
3545            index = self.all_data[self.data_index]
[cb90b65]3546        else:
3547            index = self.theory_item
[75906a1]3548        params = FittingUtilities.getStandardParam(self._model_model)
[9817207]3549        poly_params = []
3550        magnet_params = []
3551        if self.chkPolydispersity.isChecked() and self._poly_model.rowCount() > 0:
3552            poly_params = FittingUtilities.getStandardParam(self._poly_model)
3553        if self.chkMagnetism.isChecked() and self.canHaveMagnetism() and self._magnet_model.rowCount() > 0:
3554            magnet_params = FittingUtilities.getStandardParam(self._magnet_model)
[57be490]3555        report_logic = ReportPageLogic(self,
3556                                       kernel_module=self.kernel_module,
3557                                       data=self.data,
3558                                       index=index,
[9817207]3559                                       params=params+poly_params+magnet_params)
[57be490]3560
3561        return report_logic.reportList()
3562
3563    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
3564        """
3565        This is a callback method called from the CANSAS reader.
3566        We need the instance of this reader only for writing out a file,
3567        so there's nothing here.
3568        Until Load Analysis is implemented, that is.
3569        """
3570        pass
3571
3572    def loadPageState(self, pagestate=None):
3573        """
3574        Load the PageState object and update the current widget
3575        """
[6040cd7]3576        filepath = self.loadAnalysisFile()
3577        if filepath is None or filepath == "":
3578            return
[57be490]3579
[6040cd7]3580        with open(filepath, 'r') as statefile:
[9a42ea1]3581            #column_data = [line.rstrip().split() for line in statefile.readlines()]
3582            lines = statefile.readlines()
[57be490]3583
[6040cd7]3584        # convert into list of lists
[9a42ea1]3585        pass
[57be490]3586
[6040cd7]3587    def loadAnalysisFile(self):
3588        """
3589        Called when the "Open Project" menu item chosen.
3590        """
3591        default_name = "FitPage"+str(self.tab_id)+".fitv"
3592        wildcard = "fitv files (*.fitv)"
3593        kwargs = {
3594            'caption'   : 'Open Analysis',
3595            'directory' : default_name,
3596            'filter'    : wildcard,
3597            'parent'    : self,
3598        }
3599        filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
3600        return filename
[57be490]3601
[d2007a8]3602    def onCopyToClipboard(self, format=None):
3603        """
3604        Copy current fitting parameters into the clipboard
3605        using requested formatting:
3606        plain, excel, latex
3607        """
3608        param_list = self.getFitParameters()
3609        if format=="":
[9a42ea1]3610            param_list = self.getFitPage()
3611            param_list += self.getFitModel()
[d2007a8]3612            formatted_output = FittingUtilities.formatParameters(param_list)
3613        elif format == "Excel":
3614            formatted_output = FittingUtilities.formatParametersExcel(param_list[1:])
3615        elif format == "Latex":
3616            formatted_output = FittingUtilities.formatParametersLatex(param_list[1:])
3617        else:
3618            raise AttributeError("Bad parameter output format specifier.")
3619
3620        # Dump formatted_output to the clipboard
3621        cb = QtWidgets.QApplication.clipboard()
3622        cb.setText(formatted_output)
3623
[6040cd7]3624    def getFitModel(self):
[8e2cd79]3625        """
[9a42ea1]3626        serializes combobox state
[6040cd7]3627        """
3628        param_list = []
3629        model = str(self.cbModel.currentText())
3630        category = str(self.cbCategory.currentText())
3631        structure = str(self.cbStructureFactor.currentText())
3632        param_list.append(['fitpage_category', category])
3633        param_list.append(['fitpage_model', model])
3634        param_list.append(['fitpage_structure', structure])
[9a42ea1]3635
[6040cd7]3636        return param_list
3637
3638    def getFitPage(self):
3639        """
3640        serializes full state of this fit page
[8e2cd79]3641        """
3642        # run a loop over all parameters and pull out
3643        # first - regular params
[6040cd7]3644        param_list = self.getFitParameters()
3645
[9a42ea1]3646        param_list.append(['is_data', str(self.data_is_loaded)])
[17e2d502]3647        data_ids = []
3648        filenames = []
3649        if self.is_batch_fitting:
3650            for item in self.all_data:
3651                # need item->data->data_id
3652                data = GuiUtils.dataFromItem(item)
3653                data_ids.append(data.id)
3654                filenames.append(data.filename)
3655        else:
3656            if self.data_is_loaded:
3657                data_ids = [str(self.logic.data.id)]
3658                filenames = [str(self.logic.data.filename)]
3659        param_list.append(['is_batch_fitting', str(self.is_batch_fitting)])
3660        param_list.append(['data_name', filenames])
3661        param_list.append(['data_id', data_ids])
[e5ae812]3662        param_list.append(['tab_name', self.modelName()])
[6040cd7]3663        # option tab
3664        param_list.append(['q_range_min', str(self.q_range_min)])
3665        param_list.append(['q_range_max', str(self.q_range_max)])
3666        param_list.append(['q_weighting', str(self.weighting)])
[9a42ea1]3667        param_list.append(['weighting', str(self.options_widget.weighting)])
[6040cd7]3668
3669        # resolution
3670        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3671        index = self.smearing_widget.cbSmearing.currentIndex()
3672        param_list.append(['smearing', str(index)])
3673        param_list.append(['smearing_min', str(smearing_min)])
3674        param_list.append(['smearing_max', str(smearing_max)])
3675
3676        # checkboxes, if required
3677        has_polydisp = self.chkPolydispersity.isChecked()
3678        has_magnetism = self.chkMagnetism.isChecked()
3679        has_chain = self.chkChainFit.isChecked()
3680        has_2D = self.chk2DView.isChecked()
3681        param_list.append(['polydisperse_params', str(has_polydisp)])
3682        param_list.append(['magnetic_params', str(has_magnetism)])
3683        param_list.append(['chainfit_params', str(has_chain)])
3684        param_list.append(['2D_params', str(has_2D)])
3685
3686        return param_list
3687
3688    def getFitParameters(self):
3689        """
3690        serializes current parameters
3691        """
[8e2cd79]3692        param_list = []
[2eeda93]3693        if self.kernel_module is None:
3694            return param_list
3695
[0eff615]3696        param_list.append(['model_name', str(self.cbModel.currentText())])
[fc9d9a3]3697
[8e2cd79]3698        def gatherParams(row):
3699            """
3700            Create list of main parameters based on _model_model
3701            """
3702            param_name = str(self._model_model.item(row, 0).text())
[685602a]3703
[d2007a8]3704            # Assure this is a parameter - must contain a checkbox
3705            if not self._model_model.item(row, 0).isCheckable():
3706                # maybe it is a combobox item (multiplicity)
3707                try:
3708                    index = self._model_model.index(row, 1)
3709                    widget = self.lstParams.indexWidget(index)
3710                    if widget is None:
3711                        return
3712                    if isinstance(widget, QtWidgets.QComboBox):
3713                        # find the index of the combobox
3714                        current_index = widget.currentIndex()
3715                        param_list.append([param_name, 'None', str(current_index)])
3716                except Exception as ex:
3717                    pass
3718                return
3719
3720            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3721            # Value of the parameter. In some cases this is the text of the combobox choice.
3722            param_value = str(self._model_model.item(row, 1).text())
3723            param_error = None
3724            param_min = None
3725            param_max = None
3726            column_offset = 0
3727            if self.has_error_column:
3728                column_offset = 1
[6040cd7]3729                param_error = str(self._model_model.item(row, 1+column_offset).text())
[d2007a8]3730            try:
3731                param_min = str(self._model_model.item(row, 2+column_offset).text())
3732                param_max = str(self._model_model.item(row, 3+column_offset).text())
3733            except:
[fc9d9a3]3734                pass
[e5ae812]3735            # Do we have any constraints on this parameter?
3736            constraint = self.getConstraintForRow(row)
3737            cons = ()
3738            if constraint is not None:
3739                value = constraint.value
3740                func = constraint.func
[baeac95]3741                value_ex = constraint.value_ex
3742                param = constraint.param
[09e0c32]3743                validate = constraint.validate
[baeac95]3744
[09e0c32]3745                cons = (value, param, value_ex, validate, func)
[21fa45b]3746
[09e0c32]3747            param_list.append([param_name, param_checked, param_value,param_error, param_min, param_max, cons])
[8e2cd79]3748
3749        def gatherPolyParams(row):
3750            """
3751            Create list of polydisperse parameters based on _poly_model
3752            """
3753            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
3754            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3755            param_value = str(self._poly_model.item(row, 1).text())
3756            param_error = None
3757            column_offset = 0
3758            if self.has_poly_error_column:
3759                column_offset = 1
[6040cd7]3760                param_error = str(self._poly_model.item(row, 1+column_offset).text())
[8e2cd79]3761            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
3762            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
3763            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
3764            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
3765            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
[d2007a8]3766            index = self._poly_model.index(row, 6+column_offset)
3767            widget = self.lstPoly.indexWidget(index)
3768            if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3769                param_fun = widget.currentText()
[8e2cd79]3770            # width
3771            name = param_name+".width"
[6040cd7]3772            param_list.append([name, param_checked, param_value, param_error,
3773                               param_min, param_max, param_npts, param_nsigs, param_fun])
[8e2cd79]3774
3775        def gatherMagnetParams(row):
3776            """
3777            Create list of magnetic parameters based on _magnet_model
3778            """
3779            param_name = str(self._magnet_model.item(row, 0).text())
3780            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3781            param_value = str(self._magnet_model.item(row, 1).text())
3782            param_error = None
3783            column_offset = 0
3784            if self.has_magnet_error_column:
3785                column_offset = 1
[6040cd7]3786                param_error = str(self._magnet_model.item(row, 1+column_offset).text())
[8e2cd79]3787            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
3788            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
[6040cd7]3789            param_list.append([param_name, param_checked, param_value,
3790                               param_error, param_min, param_max])
[8e2cd79]3791
3792        self.iterateOverModel(gatherParams)
3793        if self.chkPolydispersity.isChecked():
3794            self.iterateOverPolyModel(gatherPolyParams)
[a98014ab]3795        if self.chkMagnetism.isChecked() and self.canHaveMagnetism():
[b259485]3796            self.iterateOverMagnetModel(gatherMagnetParams)
[8e2cd79]3797
[d2007a8]3798        if self.kernel_module.is_multiplicity_model:
3799            param_list.append(['multiplicity', str(self.kernel_module.multiplicity)])
[8e2cd79]3800
[d2007a8]3801        return param_list
[8e2cd79]3802
3803    def onParameterPaste(self):
3804        """
3805        Use the clipboard to update fit state
3806        """
3807        # Check if the clipboard contains right stuff
3808        cb = QtWidgets.QApplication.clipboard()
3809        cb_text = cb.text()
3810
3811        lines = cb_text.split(':')
3812        if lines[0] != 'sasview_parameter_values':
3813            return False
[0eff615]3814
[d2007a8]3815        # put the text into dictionary
3816        line_dict = {}
3817        for line in lines[1:]:
3818            content = line.split(',')
3819            if len(content) > 1:
3820                line_dict[content[0]] = content[1:]
[0eff615]3821
[2eeda93]3822        self.updatePageWithParameters(line_dict)
[21fa45b]3823
[2eeda93]3824    def createPageForParameters(self, line_dict):
3825        """
3826        Sets up page with requested model/str factor
3827        and fills it up with sent parameters
3828        """
3829        if 'fitpage_category' in line_dict:
3830            self.cbCategory.setCurrentIndex(self.cbCategory.findText(line_dict['fitpage_category'][0]))
3831        if 'fitpage_model' in line_dict:
3832            self.cbModel.setCurrentIndex(self.cbModel.findText(line_dict['fitpage_model'][0]))
3833        if 'fitpage_structure' in line_dict:
3834            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(line_dict['fitpage_structure'][0]))
3835
3836        # Now that the page is ready for parameters, fill it up
3837        self.updatePageWithParameters(line_dict)
[21fa45b]3838
[2eeda93]3839    def updatePageWithParameters(self, line_dict):
3840        """
3841        Update FitPage with parameters in line_dict
3842        """
[d2007a8]3843        if 'model_name' not in line_dict.keys():
[2eeda93]3844            return
3845        model = line_dict['model_name'][0]
3846        context = {}
[21fa45b]3847
[d2007a8]3848        if 'multiplicity' in line_dict.keys():
3849            multip = int(line_dict['multiplicity'][0], 0)
3850            # reset the model with multiplicity, so further updates are saved
3851            if self.kernel_module.is_multiplicity_model:
3852                self.kernel_module.multiplicity=multip
3853                self.updateMultiplicityCombo(multip)
3854
[e5ae812]3855        if 'tab_name' in line_dict.keys():
3856            self.kernel_module.name = line_dict['tab_name'][0]
[d2007a8]3857        if 'polydisperse_params' in line_dict.keys():
3858            self.chkPolydispersity.setChecked(line_dict['polydisperse_params'][0]=='True')
3859        if 'magnetic_params' in line_dict.keys():
3860            self.chkMagnetism.setChecked(line_dict['magnetic_params'][0]=='True')
3861        if 'chainfit_params' in line_dict.keys():
3862            self.chkChainFit.setChecked(line_dict['chainfit_params'][0]=='True')
3863        if '2D_params' in line_dict.keys():
3864            self.chk2DView.setChecked(line_dict['2D_params'][0]=='True')
3865
3866        # Create the context dictionary for parameters
3867        context['model_name'] = model
3868        for key, value in line_dict.items():
3869            if len(value) > 2:
3870                context[key] = value
3871
3872        if str(self.cbModel.currentText()) != str(context['model_name']):
[0eff615]3873            msg = QtWidgets.QMessageBox()
3874            msg.setIcon(QtWidgets.QMessageBox.Information)
3875            msg.setText("The model in the clipboard is not the same as the currently loaded model. \
3876                         Not all parameters saved may paste correctly.")
3877            msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
3878            result = msg.exec_()
3879            if result == QtWidgets.QMessageBox.Ok:
3880                pass
3881            else:
3882                return
3883
[d2007a8]3884        if 'smearing' in line_dict.keys():
3885            try:
3886                index = int(line_dict['smearing'][0])
3887                self.smearing_widget.cbSmearing.setCurrentIndex(index)
3888            except ValueError:
3889                pass
3890        if 'smearing_min' in line_dict.keys():
3891            try:
3892                self.smearing_widget.dq_l = float(line_dict['smearing_min'][0])
3893            except ValueError:
3894                pass
3895        if 'smearing_max' in line_dict.keys():
3896            try:
3897                self.smearing_widget.dq_r = float(line_dict['smearing_max'][0])
3898            except ValueError:
3899                pass
3900
3901        if 'q_range_max' in line_dict.keys():
3902            try:
3903                self.q_range_min = float(line_dict['q_range_min'][0])
3904                self.q_range_max = float(line_dict['q_range_max'][0])
3905            except ValueError:
3906                pass
3907        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
[9a42ea1]3908        try:
3909            button_id = int(line_dict['weighting'][0])
3910            for button in self.options_widget.weightingGroup.buttons():
3911                if abs(self.options_widget.weightingGroup.id(button)) == button_id+2:
3912                    button.setChecked(True)
3913                    break
3914        except ValueError:
3915            pass
[d2007a8]3916
[8e2cd79]3917        self.updateFullModel(context)
3918        self.updateFullPolyModel(context)
[d2007a8]3919        self.updateFullMagnetModel(context)
3920
3921    def updateMultiplicityCombo(self, multip):
3922        """
3923        Find and update the multiplicity combobox
3924        """
3925        index = self._model_model.index(self._n_shells_row, 1)
3926        widget = self.lstParams.indexWidget(index)
3927        if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3928            widget.setCurrentIndex(widget.findText(str(multip)))
3929        self.current_shell_displayed = multip
[8e2cd79]3930
3931    def updateFullModel(self, param_dict):
3932        """
3933        Update the model with new parameters
3934        """
3935        assert isinstance(param_dict, dict)
3936        if not dict:
3937            return
3938
3939        def updateFittedValues(row):
3940            # Utility function for main model update
3941            # internal so can use closure for param_dict
3942            param_name = str(self._model_model.item(row, 0).text())
3943            if param_name not in list(param_dict.keys()):
3944                return
[d2007a8]3945            # Special case of combo box in the cell (multiplicity)
3946            param_line = param_dict[param_name]
3947            if len(param_line) == 1:
3948                # modify the shells value
3949                try:
3950                    combo_index = int(param_line[0])
3951                except ValueError:
3952                    # quietly pass
3953                    return
3954                index = self._model_model.index(row, 1)
3955                widget = self.lstParams.indexWidget(index)
3956                if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3957                    #widget.setCurrentIndex(combo_index)
3958                    return
[8e2cd79]3959            # checkbox state
3960            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3961            self._model_model.item(row, 0).setCheckState(param_checked)
3962
[d2007a8]3963            # parameter value can be either just a value or text on the combobox
3964            param_text = param_dict[param_name][1]
3965            index = self._model_model.index(row, 1)
3966            widget = self.lstParams.indexWidget(index)
3967            if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3968                # Find the right index based on text
3969                combo_index = int(param_text, 0)
3970                widget.setCurrentIndex(combo_index)
3971            else:
3972                # modify the param value
3973                param_repr = GuiUtils.formatNumber(param_text, high=True)
3974                self._model_model.item(row, 1).setText(param_repr)
[8e2cd79]3975
3976            # Potentially the error column
3977            ioffset = 0
[6040cd7]3978            joffset = 0
[e5ae812]3979            if len(param_dict[param_name])>5:
[8e2cd79]3980                # error values are not editable - no need to update
3981                ioffset = 1
[6040cd7]3982            if self.has_error_column:
3983                joffset = 1
[8e2cd79]3984            # min/max
[685602a]3985            try:
3986                param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
[6040cd7]3987                self._model_model.item(row, 2+joffset).setText(param_repr)
[685602a]3988                param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
[6040cd7]3989                self._model_model.item(row, 3+joffset).setText(param_repr)
[685602a]3990            except:
3991                pass
3992
[e5ae812]3993            # constraints
3994            cons = param_dict[param_name][4+ioffset]
[09e0c32]3995            if cons is not None and len(cons)==5:
[e5ae812]3996                value = cons[0]
[baeac95]3997                param = cons[1]
3998                value_ex = cons[2]
[09e0c32]3999                validate = cons[3]
4000                function = cons[4]
[e5ae812]4001                constraint = Constraint()
4002                constraint.value = value
4003                constraint.func = function
[baeac95]4004                constraint.param = param
4005                constraint.value_ex = value_ex
[09e0c32]4006                constraint.validate = validate
[e5ae812]4007                self.addConstraintToRow(constraint=constraint, row=row)
4008
[0eff615]4009            self.setFocus()
4010
[8e2cd79]4011        self.iterateOverModel(updateFittedValues)
[0eff615]4012
[8e2cd79]4013    def updateFullPolyModel(self, param_dict):
4014        """
4015        Update the polydispersity model with new parameters, create the errors column
4016        """
4017        assert isinstance(param_dict, dict)
4018        if not dict:
4019            return
4020
4021        def updateFittedValues(row):
4022            # Utility function for main model update
4023            # internal so can use closure for param_dict
4024            if row >= self._poly_model.rowCount():
4025                return
4026            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
4027            if param_name not in list(param_dict.keys()):
4028                return
4029            # checkbox state
4030            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
4031            self._poly_model.item(row,0).setCheckState(param_checked)
4032
4033            # modify the param value
4034            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
4035            self._poly_model.item(row, 1).setText(param_repr)
4036
4037            # Potentially the error column
4038            ioffset = 0
[6040cd7]4039            joffset = 0
[1e0296b]4040            if len(param_dict[param_name])>7:
[8e2cd79]4041                ioffset = 1
[6040cd7]4042            if self.has_poly_error_column:
4043                joffset = 1
[8e2cd79]4044            # min
4045            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
[6040cd7]4046            self._poly_model.item(row, 2+joffset).setText(param_repr)
[8e2cd79]4047            # max
4048            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
[6040cd7]4049            self._poly_model.item(row, 3+joffset).setText(param_repr)
[8e2cd79]4050            # Npts
4051            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
[6040cd7]4052            self._poly_model.item(row, 4+joffset).setText(param_repr)
[8e2cd79]4053            # Nsigs
4054            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
[6040cd7]4055            self._poly_model.item(row, 5+joffset).setText(param_repr)
[8e2cd79]4056
[0eff615]4057            self.setFocus()
[8e2cd79]4058
4059        self.iterateOverPolyModel(updateFittedValues)
4060
[d2007a8]4061    def updateFullMagnetModel(self, param_dict):
4062        """
4063        Update the magnetism model with new parameters, create the errors column
4064        """
4065        assert isinstance(param_dict, dict)
4066        if not dict:
4067            return
4068
4069        def updateFittedValues(row):
4070            # Utility function for main model update
4071            # internal so can use closure for param_dict
4072            if row >= self._magnet_model.rowCount():
4073                return
4074            param_name = str(self._magnet_model.item(row, 0).text()).rsplit()[-1]
4075            if param_name not in list(param_dict.keys()):
4076                return
4077            # checkbox state
4078            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
4079            self._magnet_model.item(row,0).setCheckState(param_checked)
4080
4081            # modify the param value
4082            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
4083            self._magnet_model.item(row, 1).setText(param_repr)
[339e22b]4084
[d2007a8]4085            # Potentially the error column
4086            ioffset = 0
[6040cd7]4087            joffset = 0
[1e0296b]4088            if len(param_dict[param_name])>4:
[d2007a8]4089                ioffset = 1
[6040cd7]4090            if self.has_magnet_error_column:
4091                joffset = 1
[d2007a8]4092            # min
4093            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
[6040cd7]4094            self._magnet_model.item(row, 2+joffset).setText(param_repr)
[d2007a8]4095            # max
4096            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
[6040cd7]4097            self._magnet_model.item(row, 3+joffset).setText(param_repr)
[339e22b]4098
[d2007a8]4099        self.iterateOverMagnetModel(updateFittedValues)
[6040cd7]4100
4101    def getCurrentFitState(self, state=None):
4102        """
4103        Store current state for fit_page
4104        """
4105        # save model option
4106        #if self.model is not None:
4107        #    self.disp_list = self.getDispParamList()
4108        #    state.disp_list = copy.deepcopy(self.disp_list)
4109        #    #state.model = self.model.clone()
4110
4111        # Comboboxes
4112        state.categorycombobox = self.cbCategory.currentText()
4113        state.formfactorcombobox = self.cbModel.currentText()
4114        if self.cbStructureFactor.isEnabled():
4115            state.structurecombobox = self.cbStructureFactor.currentText()
4116        state.tcChi = self.chi2
4117
4118        state.enable2D = self.is2D
4119
4120        #state.weights = copy.deepcopy(self.weights)
4121        # save data
4122        state.data = copy.deepcopy(self.data)
4123
4124        # save plotting range
4125        state.qmin = self.q_range_min
4126        state.qmax = self.q_range_max
4127        state.npts = self.npts
4128
4129        #    self.state.enable_disp = self.enable_disp.GetValue()
4130        #    self.state.disable_disp = self.disable_disp.GetValue()
4131
4132        #    self.state.enable_smearer = \
4133        #                        copy.deepcopy(self.enable_smearer.GetValue())
4134        #    self.state.disable_smearer = \
4135        #                        copy.deepcopy(self.disable_smearer.GetValue())
4136
4137        #self.state.pinhole_smearer = \
4138        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
4139        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
4140        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
4141        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
4142        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
4143        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
4144
4145        p = self.model_parameters
4146        # save checkbutton state and txtcrtl values
4147        state.parameters = FittingUtilities.getStandardParam(self._model_model)
4148        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
4149
4150        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
4151        #self._copy_parameters_state(self.parameters, self.state.parameters)
4152        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
4153        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
4154
Note: See TracBrowser for help on using the repository browser.