source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 66d4370

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 66d4370 was 66d4370, checked in by Piotr Rozyczko <rozyczko@…>, 12 months ago

Separate kernel parameters so they can be selectively added during fitting and calculation. SASVIEW-1008, SASVIEW-1013

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