source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 343d7fd

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 343d7fd was f3cc979, checked in by piotr, 6 years ago

Add orientation_parameters to the update list for polydisp. display in main table. SASVIEW-1013

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