source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 86d3207

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 86d3207 was 86d3207, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Added SLD profile view. SASVIEW-1050

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