source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 557fc498

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 557fc498 was 557fc498, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Added GUI elements disablement/enablement on fit and calculate SASVIEW-1064

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