source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 73665a8

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

block main model update when sending read-only polydisp. data.

  • Property mode set to 100644
File size: 124.4 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        # update in param model
1200        if model_column in [delegate.poly_pd, delegate.poly_error, delegate.poly_min, delegate.poly_max]:
1201            row = self.getRowFromName(parameter_name)
1202            param_item = self._model_model.item(row)
1203            self._model_model.blockSignals(True)
1204            param_item.child(0).child(0, model_column).setText(item.text())
1205            self._model_model.blockSignals(False)
1206
1207    def onMagnetModelChange(self, item):
1208        """
1209        Callback method for updating the sasmodel magnetic parameters with the GUI values
1210        """
1211        model_column = item.column()
1212        model_row = item.row()
1213        name_index = self._magnet_model.index(model_row, 0)
1214        parameter_name = str(self._magnet_model.data(name_index))
1215
1216        if model_column == 0:
1217            value = item.checkState()
1218            if value == QtCore.Qt.Checked:
1219                self.magnet_params_to_fit.append(parameter_name)
1220            else:
1221                if parameter_name in self.magnet_params_to_fit:
1222                    self.magnet_params_to_fit.remove(parameter_name)
1223            self.cmdFit.setEnabled(self.haveParamsToFit())
1224            # Update state stack
1225            self.updateUndo()
1226            return
1227
1228        # Extract changed value.
1229        try:
1230            value = GuiUtils.toDouble(item.text())
1231        except TypeError:
1232            # Unparsable field
1233            return
1234        delegate = self.lstMagnetic.itemDelegate()
1235
1236        if model_column > 1:
1237            if model_column == delegate.mag_min:
1238                pos = 1
1239            elif model_column == delegate.mag_max:
1240                pos = 2
1241            elif model_column == delegate.mag_unit:
1242                pos = 0
1243            else:
1244                raise AttributeError("Wrong column in magnetism table.")
1245            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1246            self.kernel_module.details[parameter_name][pos] = value
1247        else:
1248            self.magnet_params[parameter_name] = value
1249            #self.kernel_module.setParam(parameter_name) = value
1250            # Force the chart update when actual parameters changed
1251            self.recalculatePlotData()
1252
1253        # Update state stack
1254        self.updateUndo()
1255
1256    def onHelp(self):
1257        """
1258        Show the "Fitting" section of help
1259        """
1260        tree_location = "/user/qtgui/Perspectives/Fitting/"
1261
1262        # Actual file will depend on the current tab
1263        tab_id = self.tabFitting.currentIndex()
1264        helpfile = "fitting.html"
1265        if tab_id == 0:
1266            helpfile = "fitting_help.html"
1267        elif tab_id == 1:
1268            helpfile = "residuals_help.html"
1269        elif tab_id == 2:
1270            helpfile = "resolution.html"
1271        elif tab_id == 3:
1272            helpfile = "pd/polydispersity.html"
1273        elif tab_id == 4:
1274            helpfile = "magnetism/magnetism.html"
1275        help_location = tree_location + helpfile
1276
1277        self.showHelp(help_location)
1278
1279    def showHelp(self, url):
1280        """
1281        Calls parent's method for opening an HTML page
1282        """
1283        self.parent.showHelp(url)
1284
1285    def onDisplayMagneticAngles(self):
1286        """
1287        Display a simple image showing direction of magnetic angles
1288        """
1289        self.magneticAnglesWidget.show()
1290
1291    def onFit(self):
1292        """
1293        Perform fitting on the current data
1294        """
1295        if self.fit_started:
1296            self.stopFit()
1297            return
1298
1299        # initialize fitter constants
1300        fit_id = 0
1301        handler = None
1302        batch_inputs = {}
1303        batch_outputs = {}
1304        #---------------------------------
1305        if LocalConfig.USING_TWISTED:
1306            handler = None
1307            updater = None
1308        else:
1309            handler = ConsoleUpdate(parent=self.parent,
1310                                    manager=self,
1311                                    improvement_delta=0.1)
1312            updater = handler.update_fit
1313
1314        # Prepare the fitter object
1315        try:
1316            fitters, _ = self.prepareFitters()
1317        except ValueError as ex:
1318            # This should not happen! GUI explicitly forbids this situation
1319            self.communicate.statusBarUpdateSignal.emit(str(ex))
1320            return
1321
1322        # keep local copy of kernel parameters, as they will change during the update
1323        self.kernel_module_copy = copy.deepcopy(self.kernel_module)
1324
1325        # Create the fitting thread, based on the fitter
1326        completefn = self.batchFittingCompleted if self.is_batch_fitting else self.fittingCompleted
1327
1328        self.calc_fit = FitThread(handler=handler,
1329                            fn=fitters,
1330                            batch_inputs=batch_inputs,
1331                            batch_outputs=batch_outputs,
1332                            page_id=[[self.page_id]],
1333                            updatefn=updater,
1334                            completefn=completefn,
1335                            reset_flag=self.is_chain_fitting)
1336
1337        if LocalConfig.USING_TWISTED:
1338            # start the trhrhread with twisted
1339            calc_thread = threads.deferToThread(self.calc_fit.compute)
1340            calc_thread.addCallback(completefn)
1341            calc_thread.addErrback(self.fitFailed)
1342        else:
1343            # Use the old python threads + Queue
1344            self.calc_fit.queue()
1345            self.calc_fit.ready(2.5)
1346
1347        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
1348        self.fit_started = True
1349        # Disable some elements
1350        self.setFittingStarted()
1351
1352    def stopFit(self):
1353        """
1354        Attempt to stop the fitting thread
1355        """
1356        if self.calc_fit is None or not self.calc_fit.isrunning():
1357            return
1358        self.calc_fit.stop()
1359        #self.fit_started=False
1360        #re-enable the Fit button
1361        self.setFittingStopped()
1362
1363        msg = "Fitting cancelled."
1364        self.communicate.statusBarUpdateSignal.emit(msg)
1365
1366    def updateFit(self):
1367        """
1368        """
1369        print("UPDATE FIT")
1370        pass
1371
1372    def fitFailed(self, reason):
1373        """
1374        """
1375        self.setFittingStopped()
1376        msg = "Fitting failed with: "+ str(reason)
1377        self.communicate.statusBarUpdateSignal.emit(msg)
1378
1379    def batchFittingCompleted(self, result):
1380        """
1381        Send the finish message from calculate threads to main thread
1382        """
1383        if result is None:
1384            result = tuple()
1385        self.batchFittingFinishedSignal.emit(result)
1386
1387    def batchFitComplete(self, result):
1388        """
1389        Receive and display batch fitting results
1390        """
1391        #re-enable the Fit button
1392        self.setFittingStopped()
1393
1394        if len(result) == 0:
1395            msg = "Fitting failed."
1396            self.communicate.statusBarUpdateSignal.emit(msg)
1397            return
1398
1399        # Show the grid panel
1400        self.communicate.sendDataToGridSignal.emit(result[0])
1401
1402        elapsed = result[1]
1403        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
1404        self.communicate.statusBarUpdateSignal.emit(msg)
1405
1406        # Run over the list of results and update the items
1407        for res_index, res_list in enumerate(result[0]):
1408            # results
1409            res = res_list[0]
1410            param_dict = self.paramDictFromResults(res)
1411
1412            # create local kernel_module
1413            kernel_module = FittingUtilities.updateKernelWithResults(self.kernel_module, param_dict)
1414            # pull out current data
1415            data = self._logic[res_index].data
1416
1417            # Switch indexes
1418            self.onSelectBatchFilename(res_index)
1419
1420            method = self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1421            self.calculateQGridForModelExt(data=data, model=kernel_module, completefn=method, use_threads=False)
1422
1423        # Restore original kernel_module, so subsequent fits on the same model don't pick up the new params
1424        if self.kernel_module is not None:
1425            self.kernel_module = copy.deepcopy(self.kernel_module_copy)
1426
1427    def paramDictFromResults(self, results):
1428        """
1429        Given the fit results structure, pull out optimized parameters and return them as nicely
1430        formatted dict
1431        """
1432        if results.fitness is None or \
1433            not np.isfinite(results.fitness) or \
1434            np.any(results.pvec is None) or \
1435            not np.all(np.isfinite(results.pvec)):
1436            msg = "Fitting did not converge!"
1437            self.communicate.statusBarUpdateSignal.emit(msg)
1438            msg += results.mesg
1439            logging.error(msg)
1440            return
1441
1442        param_list = results.param_list # ['radius', 'radius.width']
1443        param_values = results.pvec     # array([ 0.36221662,  0.0146783 ])
1444        param_stderr = results.stderr   # array([ 1.71293015,  1.71294233])
1445        params_and_errors = list(zip(param_values, param_stderr))
1446        param_dict = dict(zip(param_list, params_and_errors))
1447
1448        return param_dict
1449
1450    def fittingCompleted(self, result):
1451        """
1452        Send the finish message from calculate threads to main thread
1453        """
1454        if result is None:
1455            result = tuple()
1456        self.fittingFinishedSignal.emit(result)
1457
1458    def fitComplete(self, result):
1459        """
1460        Receive and display fitting results
1461        "result" is a tuple of actual result list and the fit time in seconds
1462        """
1463        #re-enable the Fit button
1464        self.setFittingStopped()
1465
1466        if len(result) == 0:
1467            msg = "Fitting failed."
1468            self.communicate.statusBarUpdateSignal.emit(msg)
1469            return
1470
1471        res_list = result[0][0]
1472        res = res_list[0]
1473        self.chi2 = res.fitness
1474        param_dict = self.paramDictFromResults(res)
1475
1476        if param_dict is None:
1477            return
1478
1479        elapsed = result[1]
1480        if self.calc_fit._interrupting:
1481            msg = "Fitting cancelled by user after: %s s." % GuiUtils.formatNumber(elapsed)
1482            logging.warning("\n"+msg+"\n")
1483        else:
1484            msg = "Fitting completed successfully in: %s s." % GuiUtils.formatNumber(elapsed)
1485        self.communicate.statusBarUpdateSignal.emit(msg)
1486
1487        # Dictionary of fitted parameter: value, error
1488        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
1489        self.updateModelFromList(param_dict)
1490
1491        self.updatePolyModelFromList(param_dict)
1492
1493        self.updateMagnetModelFromList(param_dict)
1494
1495        # update charts
1496        self.onPlot()
1497
1498        # Read only value - we can get away by just printing it here
1499        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
1500        self.lblChi2Value.setText(chi2_repr)
1501
1502    def prepareFitters(self, fitter=None, fit_id=0):
1503        """
1504        Prepare the Fitter object for use in fitting
1505        """
1506        # fitter = None -> single/batch fitting
1507        # fitter = Fit() -> simultaneous fitting
1508
1509        # Data going in
1510        data = self.logic.data
1511        model = copy.deepcopy(self.kernel_module)
1512        qmin = self.q_range_min
1513        qmax = self.q_range_max
1514        # add polydisperse/magnet parameters if asked
1515        self.updateKernelModelWithExtraParams(model)
1516
1517        params_to_fit = self.main_params_to_fit
1518        if self.chkPolydispersity.isChecked():
1519            params_to_fit += self.poly_params_to_fit
1520        if self.chkMagnetism.isChecked():
1521            params_to_fit += self.magnet_params_to_fit
1522        if not params_to_fit:
1523            raise ValueError('Fitting requires at least one parameter to optimize.')
1524
1525        # Get the constraints.
1526        constraints = self.getComplexConstraintsForModel()
1527        if fitter is None:
1528            # For single fits - check for inter-model constraints
1529            constraints = self.getConstraintsForFitting()
1530
1531        smearer = self.smearing_widget.smearer()
1532        handler = None
1533        batch_inputs = {}
1534        batch_outputs = {}
1535
1536        fitters = []
1537        for fit_index in self.all_data:
1538            fitter_single = Fit() if fitter is None else fitter
1539            data = GuiUtils.dataFromItem(fit_index)
1540            # Potential weights added directly to data
1541            weighted_data = self.addWeightingToData(data)
1542            try:
1543                fitter_single.set_model(model, fit_id, params_to_fit, data=weighted_data,
1544                             constraints=constraints)
1545            except ValueError as ex:
1546                raise ValueError("Setting model parameters failed with: %s" % ex)
1547
1548            qmin, qmax, _ = self.logic.computeRangeFromData(weighted_data)
1549            fitter_single.set_data(data=weighted_data, id=fit_id, smearer=smearer, qmin=qmin,
1550                            qmax=qmax)
1551            fitter_single.select_problem_for_fit(id=fit_id, value=1)
1552            if fitter is None:
1553                # Assign id to the new fitter only
1554                fitter_single.fitter_id = [self.page_id]
1555            fit_id += 1
1556            fitters.append(fitter_single)
1557
1558        return fitters, fit_id
1559
1560    def iterateOverModel(self, func):
1561        """
1562        Take func and throw it inside the model row loop
1563        """
1564        for row_i in range(self._model_model.rowCount()):
1565            func(row_i)
1566
1567    def updateModelFromList(self, param_dict):
1568        """
1569        Update the model with new parameters, create the errors column
1570        """
1571        assert isinstance(param_dict, dict)
1572        if not dict:
1573            return
1574
1575        def updateFittedValues(row):
1576            # Utility function for main model update
1577            # internal so can use closure for param_dict
1578            param_name = str(self._model_model.item(row, 0).text())
1579            if param_name not in list(param_dict.keys()):
1580                return
1581            # modify the param value
1582            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1583            self._model_model.item(row, 1).setText(param_repr)
1584            if self.has_error_column:
1585                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1586                self._model_model.item(row, 2).setText(error_repr)
1587
1588        def updatePolyValues(row):
1589            # Utility function for updateof polydispersity part of the main model
1590            param_name = str(self._model_model.item(row, 0).text())+'.width'
1591            if param_name not in list(param_dict.keys()):
1592                return
1593            # modify the param value
1594            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1595            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
1596            # modify the param error
1597            if self.has_error_column:
1598                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1599                self._model_model.item(row, 0).child(0).child(0,2).setText(error_repr)
1600
1601        def createErrorColumn(row):
1602            # Utility function for error column update
1603            item = QtGui.QStandardItem()
1604            def createItem(param_name):
1605                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1606                item.setText(error_repr)
1607            def curr_param():
1608                return str(self._model_model.item(row, 0).text())
1609
1610            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1611
1612            error_column.append(item)
1613
1614        def createPolyErrorColumn(row):
1615            # Utility function for error column update in the polydispersity sub-rows
1616            # NOTE: only creates empty items; updatePolyValues adds the error value
1617            item = self._model_model.item(row, 0)
1618            if not item.hasChildren():
1619                return
1620            poly_item = item.child(0)
1621            if not poly_item.hasChildren():
1622                return
1623            poly_item.insertColumn(2, [QtGui.QStandardItem("")])
1624
1625        # block signals temporarily, so we don't end up
1626        # updating charts with every single model change on the end of fitting
1627        self._model_model.blockSignals(True)
1628
1629        if not self.has_error_column:
1630            # create top-level error column
1631            error_column = []
1632            self.lstParams.itemDelegate().addErrorColumn()
1633            self.iterateOverModel(createErrorColumn)
1634
1635            # we need to enable signals for this, otherwise the final column mysteriously disappears (don't ask, I don't
1636            # know)
1637            self._model_model.blockSignals(False)
1638            self._model_model.insertColumn(2, error_column)
1639            self._model_model.blockSignals(True)
1640
1641            FittingUtilities.addErrorHeadersToModel(self._model_model)
1642
1643            # create error column in polydispersity sub-rows
1644            self.iterateOverModel(createPolyErrorColumn)
1645
1646            self.has_error_column = True
1647
1648        self.iterateOverModel(updateFittedValues)
1649        self.iterateOverModel(updatePolyValues)
1650
1651        self._model_model.blockSignals(False)
1652
1653        # Adjust the table cells width.
1654        # TODO: find a way to dynamically adjust column width while resized expanding
1655        self.lstParams.resizeColumnToContents(0)
1656        self.lstParams.resizeColumnToContents(4)
1657        self.lstParams.resizeColumnToContents(5)
1658        self.lstParams.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
1659
1660    def iterateOverPolyModel(self, func):
1661        """
1662        Take func and throw it inside the poly model row loop
1663        """
1664        for row_i in range(self._poly_model.rowCount()):
1665            func(row_i)
1666
1667    def updatePolyModelFromList(self, param_dict):
1668        """
1669        Update the polydispersity model with new parameters, create the errors column
1670        """
1671        assert isinstance(param_dict, dict)
1672        if not dict:
1673            return
1674
1675        def updateFittedValues(row_i):
1676            # Utility function for main model update
1677            # internal so can use closure for param_dict
1678            if row_i >= self._poly_model.rowCount():
1679                return
1680            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1681            if param_name not in list(param_dict.keys()):
1682                return
1683            # modify the param value
1684            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1685            self._poly_model.item(row_i, 1).setText(param_repr)
1686            if self.has_poly_error_column:
1687                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1688                self._poly_model.item(row_i, 2).setText(error_repr)
1689
1690
1691        def createErrorColumn(row_i):
1692            # Utility function for error column update
1693            if row_i >= self._poly_model.rowCount():
1694                return
1695            item = QtGui.QStandardItem()
1696
1697            def createItem(param_name):
1698                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1699                item.setText(error_repr)
1700
1701            def poly_param():
1702                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1703
1704            [createItem(param_name) for param_name in list(param_dict.keys()) if poly_param() == param_name]
1705
1706            error_column.append(item)
1707
1708        # block signals temporarily, so we don't end up
1709        # updating charts with every single model change on the end of fitting
1710        self._poly_model.blockSignals(True)
1711        self.iterateOverPolyModel(updateFittedValues)
1712        self._poly_model.blockSignals(False)
1713
1714        if self.has_poly_error_column:
1715            return
1716
1717        self.lstPoly.itemDelegate().addErrorColumn()
1718        error_column = []
1719        self.iterateOverPolyModel(createErrorColumn)
1720
1721        # switch off reponse to model change
1722        self._poly_model.blockSignals(True)
1723        self._poly_model.insertColumn(2, error_column)
1724        self._poly_model.blockSignals(False)
1725        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
1726
1727        self.has_poly_error_column = True
1728
1729    def iterateOverMagnetModel(self, func):
1730        """
1731        Take func and throw it inside the magnet model row loop
1732        """
1733        for row_i in range(self._magnet_model.rowCount()):
1734            func(row_i)
1735
1736    def updateMagnetModelFromList(self, param_dict):
1737        """
1738        Update the magnetic model with new parameters, create the errors column
1739        """
1740        assert isinstance(param_dict, dict)
1741        if not dict:
1742            return
1743        if self._magnet_model.rowCount() == 0:
1744            return
1745
1746        def updateFittedValues(row):
1747            # Utility function for main model update
1748            # internal so can use closure for param_dict
1749            if self._magnet_model.item(row, 0) is None:
1750                return
1751            param_name = str(self._magnet_model.item(row, 0).text())
1752            if param_name not in list(param_dict.keys()):
1753                return
1754            # modify the param value
1755            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1756            self._magnet_model.item(row, 1).setText(param_repr)
1757            if self.has_magnet_error_column:
1758                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1759                self._magnet_model.item(row, 2).setText(error_repr)
1760
1761        def createErrorColumn(row):
1762            # Utility function for error column update
1763            item = QtGui.QStandardItem()
1764            def createItem(param_name):
1765                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1766                item.setText(error_repr)
1767            def curr_param():
1768                return str(self._magnet_model.item(row, 0).text())
1769
1770            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1771
1772            error_column.append(item)
1773
1774        # block signals temporarily, so we don't end up
1775        # updating charts with every single model change on the end of fitting
1776        self._magnet_model.blockSignals(True)
1777        self.iterateOverMagnetModel(updateFittedValues)
1778        self._magnet_model.blockSignals(False)
1779
1780        if self.has_magnet_error_column:
1781            return
1782
1783        self.lstMagnetic.itemDelegate().addErrorColumn()
1784        error_column = []
1785        self.iterateOverMagnetModel(createErrorColumn)
1786
1787        # switch off reponse to model change
1788        self._magnet_model.blockSignals(True)
1789        self._magnet_model.insertColumn(2, error_column)
1790        self._magnet_model.blockSignals(False)
1791        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1792
1793        self.has_magnet_error_column = True
1794
1795    def onPlot(self):
1796        """
1797        Plot the current set of data
1798        """
1799        # Regardless of previous state, this should now be `plot show` functionality only
1800        self.cmdPlot.setText("Show Plot")
1801        # Force data recalculation so existing charts are updated
1802        self.recalculatePlotData()
1803        self.showPlot()
1804
1805    def onSmearingOptionsUpdate(self):
1806        """
1807        React to changes in the smearing widget
1808        """
1809        self.calculateQGridForModel()
1810
1811    def recalculatePlotData(self):
1812        """
1813        Generate a new dataset for model
1814        """
1815        if not self.data_is_loaded:
1816            self.createDefaultDataset()
1817        self.calculateQGridForModel()
1818
1819    def showPlot(self):
1820        """
1821        Show the current plot in MPL
1822        """
1823        # Show the chart if ready
1824        data_to_show = self.data if self.data_is_loaded else self.model_data
1825        if data_to_show is not None:
1826            self.communicate.plotRequestedSignal.emit([data_to_show])
1827
1828    def onOptionsUpdate(self):
1829        """
1830        Update local option values and replot
1831        """
1832        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1833            self.options_widget.state()
1834        # set Q range labels on the main tab
1835        self.lblMinRangeDef.setText(str(self.q_range_min))
1836        self.lblMaxRangeDef.setText(str(self.q_range_max))
1837        self.recalculatePlotData()
1838
1839    def setDefaultStructureCombo(self):
1840        """
1841        Fill in the structure factors combo box with defaults
1842        """
1843        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1844        factors = [factor[0] for factor in structure_factor_list]
1845        factors.insert(0, STRUCTURE_DEFAULT)
1846        self.cbStructureFactor.clear()
1847        self.cbStructureFactor.addItems(sorted(factors))
1848
1849    def createDefaultDataset(self):
1850        """
1851        Generate default Dataset 1D/2D for the given model
1852        """
1853        # Create default datasets if no data passed
1854        if self.is2D:
1855            qmax = self.q_range_max/np.sqrt(2)
1856            qstep = self.npts
1857            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
1858            return
1859        elif self.log_points:
1860            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
1861            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
1862            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
1863        else:
1864            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
1865                                   num=self.npts, endpoint=True)
1866        self.logic.createDefault1dData(interval, self.tab_id)
1867
1868    def readCategoryInfo(self):
1869        """
1870        Reads the categories in from file
1871        """
1872        self.master_category_dict = defaultdict(list)
1873        self.by_model_dict = defaultdict(list)
1874        self.model_enabled_dict = defaultdict(bool)
1875
1876        categorization_file = CategoryInstaller.get_user_file()
1877        if not os.path.isfile(categorization_file):
1878            categorization_file = CategoryInstaller.get_default_file()
1879        with open(categorization_file, 'rb') as cat_file:
1880            self.master_category_dict = json.load(cat_file)
1881            self.regenerateModelDict()
1882
1883        # Load the model dict
1884        models = load_standard_models()
1885        for model in models:
1886            self.models[model.name] = model
1887
1888        self.readCustomCategoryInfo()
1889
1890    def readCustomCategoryInfo(self):
1891        """
1892        Reads the custom model category
1893        """
1894        #Looking for plugins
1895        self.plugins = list(self.custom_models.values())
1896        plugin_list = []
1897        for name, plug in self.custom_models.items():
1898            self.models[name] = plug
1899            plugin_list.append([name, True])
1900        self.master_category_dict[CATEGORY_CUSTOM] = plugin_list
1901
1902    def regenerateModelDict(self):
1903        """
1904        Regenerates self.by_model_dict which has each model name as the
1905        key and the list of categories belonging to that model
1906        along with the enabled mapping
1907        """
1908        self.by_model_dict = defaultdict(list)
1909        for category in self.master_category_dict:
1910            for (model, enabled) in self.master_category_dict[category]:
1911                self.by_model_dict[model].append(category)
1912                self.model_enabled_dict[model] = enabled
1913
1914    def addBackgroundToModel(self, model):
1915        """
1916        Adds background parameter with default values to the model
1917        """
1918        assert isinstance(model, QtGui.QStandardItemModel)
1919        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
1920        FittingUtilities.addCheckedListToModel(model, checked_list)
1921        last_row = model.rowCount()-1
1922        model.item(last_row, 0).setEditable(False)
1923        model.item(last_row, 4).setEditable(False)
1924
1925    def addScaleToModel(self, model):
1926        """
1927        Adds scale parameter with default values to the model
1928        """
1929        assert isinstance(model, QtGui.QStandardItemModel)
1930        checked_list = ['scale', '1.0', '0.0', 'inf', '']
1931        FittingUtilities.addCheckedListToModel(model, checked_list)
1932        last_row = model.rowCount()-1
1933        model.item(last_row, 0).setEditable(False)
1934        model.item(last_row, 4).setEditable(False)
1935
1936    def addWeightingToData(self, data):
1937        """
1938        Adds weighting contribution to fitting data
1939        """
1940        new_data = copy.deepcopy(data)
1941        # Send original data for weighting
1942        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
1943        if self.is2D:
1944            new_data.err_data = weight
1945        else:
1946            new_data.dy = weight
1947
1948        return new_data
1949
1950    def updateQRange(self):
1951        """
1952        Updates Q Range display
1953        """
1954        if self.data_is_loaded:
1955            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
1956        # set Q range labels on the main tab
1957        self.lblMinRangeDef.setText(str(self.q_range_min))
1958        self.lblMaxRangeDef.setText(str(self.q_range_max))
1959        # set Q range labels on the options tab
1960        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
1961
1962    def SASModelToQModel(self, model_name, structure_factor=None):
1963        """
1964        Setting model parameters into table based on selected category
1965        """
1966        # Crete/overwrite model items
1967        self._model_model.clear()
1968
1969        # First, add parameters from the main model
1970        if model_name is not None:
1971            self.fromModelToQModel(model_name)
1972
1973        # Then, add structure factor derived parameters
1974        if structure_factor is not None and structure_factor != "None":
1975            if model_name is None:
1976                # Instantiate the current sasmodel for SF-only models
1977                self.kernel_module = self.models[structure_factor]()
1978            self.fromStructureFactorToQModel(structure_factor)
1979        else:
1980            # Allow the SF combobox visibility for the given sasmodel
1981            self.enableStructureFactorControl(structure_factor)
1982            if self.cbStructureFactor.isEnabled():
1983                structure_factor = self.cbStructureFactor.currentText()
1984                self.fromStructureFactorToQModel(structure_factor)
1985
1986        # Then, add multishells
1987        if model_name is not None:
1988            # Multishell models need additional treatment
1989            self.addExtraShells()
1990
1991        # Add polydispersity to the model
1992        self.poly_params = {}
1993        self.setPolyModel()
1994        # Add magnetic parameters to the model
1995        self.magnet_params = {}
1996        self.setMagneticModel()
1997
1998        # Adjust the table cells width
1999        self.lstParams.resizeColumnToContents(0)
2000        self.lstParams.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2001
2002        # Now we claim the model has been loaded
2003        self.model_is_loaded = True
2004        # Change the model name to a monicker
2005        self.kernel_module.name = self.modelName()
2006        # Update the smearing tab
2007        self.smearing_widget.updateKernelModel(kernel_model=self.kernel_module)
2008
2009        # (Re)-create headers
2010        FittingUtilities.addHeadersToModel(self._model_model)
2011        self.lstParams.header().setFont(self.boldFont)
2012
2013        # Update Q Ranges
2014        self.updateQRange()
2015
2016    def fromModelToQModel(self, model_name):
2017        """
2018        Setting model parameters into QStandardItemModel based on selected _model_
2019        """
2020        name = model_name
2021        kernel_module = None
2022        if self.cbCategory.currentText() == CATEGORY_CUSTOM:
2023            # custom kernel load requires full path
2024            name = os.path.join(ModelUtilities.find_plugins_dir(), model_name+".py")
2025        try:
2026            kernel_module = generate.load_kernel_module(name)
2027        except ModuleNotFoundError as ex:
2028            pass
2029
2030        if kernel_module is None:
2031            # mismatch between "name" attribute and actual filename.
2032            curr_model = self.models[model_name]
2033            name, _ = os.path.splitext(os.path.basename(curr_model.filename))
2034            try:
2035                kernel_module = generate.load_kernel_module(name)
2036            except ModuleNotFoundError as ex:
2037                logging.error("Can't find the model "+ str(ex))
2038                return
2039
2040        if hasattr(kernel_module, 'parameters'):
2041            # built-in and custom models
2042            self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
2043
2044        elif hasattr(kernel_module, 'model_info'):
2045            # for sum/multiply models
2046            self.model_parameters = kernel_module.model_info.parameters
2047
2048        elif hasattr(kernel_module, 'Model') and hasattr(kernel_module.Model, "_model_info"):
2049            # this probably won't work if there's no model_info, but just in case
2050            self.model_parameters = kernel_module.Model._model_info.parameters
2051        else:
2052            # no parameters - default to blank table
2053            msg = "No parameters found in model '{}'.".format(model_name)
2054            logger.warning(msg)
2055            self.model_parameters = modelinfo.ParameterTable([])
2056
2057        # Instantiate the current sasmodel
2058        self.kernel_module = self.models[model_name]()
2059
2060        # Explicitly add scale and background with default values
2061        temp_undo_state = self.undo_supported
2062        self.undo_supported = False
2063        self.addScaleToModel(self._model_model)
2064        self.addBackgroundToModel(self._model_model)
2065        self.undo_supported = temp_undo_state
2066
2067        self.shell_names = self.shellNamesList()
2068
2069        # Update the QModel
2070        new_rows = FittingUtilities.addParametersToModel(self.model_parameters, self.kernel_module, self.is2D)
2071
2072        for row in new_rows:
2073            self._model_model.appendRow(row)
2074        # Update the counter used for multishell display
2075        self._last_model_row = self._model_model.rowCount()
2076
2077    def fromStructureFactorToQModel(self, structure_factor):
2078        """
2079        Setting model parameters into QStandardItemModel based on selected _structure factor_
2080        """
2081        if structure_factor is None or structure_factor=="None":
2082            return
2083        structure_module = generate.load_kernel_module(structure_factor)
2084        structure_parameters = modelinfo.make_parameter_table(getattr(structure_module, 'parameters', []))
2085
2086        structure_kernel = self.models[structure_factor]()
2087        form_kernel = self.kernel_module
2088
2089        self.kernel_module = MultiplicationModel(form_kernel, structure_kernel)
2090
2091        new_rows = FittingUtilities.addSimpleParametersToModel(structure_parameters, self.is2D)
2092        for row in new_rows:
2093            self._model_model.appendRow(row)
2094            # disable fitting of parameters not listed in self.kernel_module (probably radius_effective)
2095            if row[0].text() not in self.kernel_module.params.keys():
2096                row_num = self._model_model.rowCount() - 1
2097                FittingUtilities.markParameterDisabled(self._model_model, row_num)
2098
2099        # Update the counter used for multishell display
2100        self._last_model_row = self._model_model.rowCount()
2101
2102    def haveParamsToFit(self):
2103        """
2104        Finds out if there are any parameters ready to be fitted
2105        """
2106        return (self.main_params_to_fit!=[]
2107                or self.poly_params_to_fit!=[]
2108                or self.magnet_params_to_fit != []) and \
2109                self.logic.data_is_loaded
2110
2111    def onMainParamsChange(self, item):
2112        """
2113        Callback method for updating the sasmodel parameters with the GUI values
2114        """
2115        model_column = item.column()
2116
2117        if model_column == 0:
2118            self.checkboxSelected(item)
2119            self.cmdFit.setEnabled(self.haveParamsToFit())
2120            # Update state stack
2121            self.updateUndo()
2122            return
2123
2124        model_row = item.row()
2125        name_index = self._model_model.index(model_row, 0)
2126
2127        # Extract changed value.
2128        try:
2129            value = GuiUtils.toDouble(item.text())
2130        except TypeError:
2131            # Unparsable field
2132            return
2133
2134        parameter_name = str(self._model_model.data(name_index)) # sld, background etc.
2135
2136        # Update the parameter value - note: this supports +/-inf as well
2137        self.kernel_module.params[parameter_name] = value
2138
2139        # Update the parameter value - note: this supports +/-inf as well
2140        param_column = self.lstParams.itemDelegate().param_value
2141        min_column = self.lstParams.itemDelegate().param_min
2142        max_column = self.lstParams.itemDelegate().param_max
2143        if model_column == param_column:
2144            self.kernel_module.setParam(parameter_name, value)
2145        elif model_column == min_column:
2146            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
2147            self.kernel_module.details[parameter_name][1] = value
2148        elif model_column == max_column:
2149            self.kernel_module.details[parameter_name][2] = value
2150        else:
2151            # don't update the chart
2152            return
2153
2154        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
2155        # TODO: multishell params in self.kernel_module.details[??] = value
2156
2157        # Force the chart update when actual parameters changed
2158        if model_column == 1:
2159            self.recalculatePlotData()
2160
2161        # Update state stack
2162        self.updateUndo()
2163
2164    def isCheckable(self, row):
2165        return self._model_model.item(row, 0).isCheckable()
2166
2167    def checkboxSelected(self, item):
2168        # Assure we're dealing with checkboxes
2169        if not item.isCheckable():
2170            return
2171        status = item.checkState()
2172
2173        # If multiple rows selected - toggle all of them, filtering uncheckable
2174        # Switch off signaling from the model to avoid recursion
2175        self._model_model.blockSignals(True)
2176        # Convert to proper indices and set requested enablement
2177        self.setParameterSelection(status)
2178        self._model_model.blockSignals(False)
2179
2180        # update the list of parameters to fit
2181        self.main_params_to_fit = self.checkedListFromModel(self._model_model)
2182
2183    def checkedListFromModel(self, model):
2184        """
2185        Returns list of checked parameters for given model
2186        """
2187        def isChecked(row):
2188            return model.item(row, 0).checkState() == QtCore.Qt.Checked
2189
2190        return [str(model.item(row_index, 0).text())
2191                for row_index in range(model.rowCount())
2192                if isChecked(row_index)]
2193
2194    def createNewIndex(self, fitted_data):
2195        """
2196        Create a model or theory index with passed Data1D/Data2D
2197        """
2198        if self.data_is_loaded:
2199            if not fitted_data.name:
2200                name = self.nameForFittedData(self.data.filename)
2201                fitted_data.title = name
2202                fitted_data.name = name
2203                fitted_data.filename = name
2204                fitted_data.symbol = "Line"
2205            self.updateModelIndex(fitted_data)
2206        else:
2207            if not fitted_data.name:
2208                name = self.nameForFittedData(self.kernel_module.id)
2209            else:
2210                name = fitted_data.name
2211            fitted_data.title = name
2212            fitted_data.filename = name
2213            fitted_data.symbol = "Line"
2214            self.createTheoryIndex(fitted_data)
2215
2216    def updateModelIndex(self, fitted_data):
2217        """
2218        Update a QStandardModelIndex containing model data
2219        """
2220        name = self.nameFromData(fitted_data)
2221        # Make this a line if no other defined
2222        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
2223            fitted_data.symbol = 'Line'
2224        # Notify the GUI manager so it can update the main model in DataExplorer
2225        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
2226
2227    def createTheoryIndex(self, fitted_data):
2228        """
2229        Create a QStandardModelIndex containing model data
2230        """
2231        name = self.nameFromData(fitted_data)
2232        # Notify the GUI manager so it can create the theory model in DataExplorer
2233        self.theory_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
2234        self.communicate.updateTheoryFromPerspectiveSignal.emit(self.theory_item)
2235
2236    def nameFromData(self, fitted_data):
2237        """
2238        Return name for the dataset. Terribly impure function.
2239        """
2240        if fitted_data.name is None:
2241            name = self.nameForFittedData(self.logic.data.filename)
2242            fitted_data.title = name
2243            fitted_data.name = name
2244            fitted_data.filename = name
2245        else:
2246            name = fitted_data.name
2247        return name
2248
2249    def methodCalculateForData(self):
2250        '''return the method for data calculation'''
2251        return Calc1D if isinstance(self.data, Data1D) else Calc2D
2252
2253    def methodCompleteForData(self):
2254        '''return the method for result parsin on calc complete '''
2255        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
2256
2257    def updateKernelModelWithExtraParams(self, model=None):
2258        """
2259        Updates kernel model 'model' with extra parameters from
2260        the polydisp and magnetism tab, if the tabs are enabled
2261        """
2262        if model is None: return
2263        if not hasattr(model, 'setParam'): return
2264
2265        # add polydisperse parameters if asked
2266        if self.chkPolydispersity.isChecked():
2267            for key, value in self.poly_params.items():
2268                model.setParam(key, value)
2269        # add magnetic params if asked
2270        if self.chkMagnetism.isChecked():
2271            for key, value in self.magnet_params.items():
2272                model.setParam(key, value)
2273
2274    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
2275        """
2276        Wrapper for Calc1D/2D calls
2277        """
2278        if data is None:
2279            data = self.data
2280        if model is None:
2281            model = copy.deepcopy(self.kernel_module)
2282            self.updateKernelModelWithExtraParams(model)
2283
2284        if completefn is None:
2285            completefn = self.methodCompleteForData()
2286        smearer = self.smearing_widget.smearer()
2287        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2288
2289        # Awful API to a backend method.
2290        calc_thread = self.methodCalculateForData()(data=data,
2291                                               model=model,
2292                                               page_id=0,
2293                                               qmin=self.q_range_min,
2294                                               qmax=self.q_range_max,
2295                                               smearer=smearer,
2296                                               state=None,
2297                                               weight=weight,
2298                                               fid=None,
2299                                               toggle_mode_on=False,
2300                                               completefn=completefn,
2301                                               update_chisqr=True,
2302                                               exception_handler=self.calcException,
2303                                               source=None)
2304        if use_threads:
2305            if LocalConfig.USING_TWISTED:
2306                # start the thread with twisted
2307                thread = threads.deferToThread(calc_thread.compute)
2308                thread.addCallback(completefn)
2309                thread.addErrback(self.calculateDataFailed)
2310            else:
2311                # Use the old python threads + Queue
2312                calc_thread.queue()
2313                calc_thread.ready(2.5)
2314        else:
2315            results = calc_thread.compute()
2316            completefn(results)
2317
2318    def calculateQGridForModel(self):
2319        """
2320        Prepare the fitting data object, based on current ModelModel
2321        """
2322        if self.kernel_module is None:
2323            return
2324        self.calculateQGridForModelExt()
2325
2326    def calculateDataFailed(self, reason):
2327        """
2328        Thread returned error
2329        """
2330        print("Calculate Data failed with ", reason)
2331
2332    def completed1D(self, return_data):
2333        self.Calc1DFinishedSignal.emit(return_data)
2334
2335    def completed2D(self, return_data):
2336        self.Calc2DFinishedSignal.emit(return_data)
2337
2338    def complete1D(self, return_data):
2339        """
2340        Plot the current 1D data
2341        """
2342        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
2343        residuals = self.calculateResiduals(fitted_data)
2344        self.model_data = fitted_data
2345        new_plots = [fitted_data]
2346        if residuals is not None:
2347            new_plots.append(residuals)
2348
2349        if self.data_is_loaded:
2350            GuiUtils.deleteRedundantPlots(self.all_data[self.data_index], new_plots)
2351        else:
2352            # delete theory items for the model, in order to get rid of any redundant items, e.g. beta(Q), S_eff(Q)
2353            self.communicate.deleteIntermediateTheoryPlotsSignal.emit(self.kernel_module.id)
2354
2355        # Create plots for intermediate product data
2356        pq_data, sq_data = self.logic.new1DProductPlots(return_data, self.tab_id)
2357        if pq_data is not None:
2358            pq_data.symbol = "Line"
2359            self.createNewIndex(pq_data)
2360            # self.communicate.plotUpdateSignal.emit([pq_data])
2361            new_plots.append(pq_data)
2362        if sq_data is not None:
2363            sq_data.symbol = "Line"
2364            self.createNewIndex(sq_data)
2365            # self.communicate.plotUpdateSignal.emit([sq_data])
2366            new_plots.append(sq_data)
2367
2368        # Update/generate plots
2369        for plot in new_plots:
2370            self.communicate.plotUpdateSignal.emit([plot])
2371
2372    def complete2D(self, return_data):
2373        """
2374        Plot the current 2D data
2375        """
2376        fitted_data = self.logic.new2DPlot(return_data)
2377        residuals = self.calculateResiduals(fitted_data)
2378        self.model_data = fitted_data
2379        new_plots = [fitted_data]
2380        if residuals is not None:
2381            new_plots.append(residuals)
2382
2383        # Update/generate plots
2384        for plot in new_plots:
2385            self.communicate.plotUpdateSignal.emit([plot])
2386
2387    def calculateResiduals(self, fitted_data):
2388        """
2389        Calculate and print Chi2 and display chart of residuals. Returns residuals plot object.
2390        """
2391        # Create a new index for holding data
2392        fitted_data.symbol = "Line"
2393
2394        # Modify fitted_data with weighting
2395        weighted_data = self.addWeightingToData(fitted_data)
2396
2397        self.createNewIndex(weighted_data)
2398        # Calculate difference between return_data and logic.data
2399        self.chi2 = FittingUtilities.calculateChi2(weighted_data, self.logic.data)
2400        # Update the control
2401        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
2402        self.lblChi2Value.setText(chi2_repr)
2403
2404        # Plot residuals if actual data
2405        if not self.data_is_loaded:
2406            return
2407
2408        residuals_plot = FittingUtilities.plotResiduals(self.data, weighted_data)
2409        residuals_plot.id = "Residual " + residuals_plot.id
2410        self.createNewIndex(residuals_plot)
2411        return residuals_plot
2412
2413    def onCategoriesChanged(self):
2414            """
2415            Reload the category/model comboboxes
2416            """
2417            # Store the current combo indices
2418            current_cat = self.cbCategory.currentText()
2419            current_model = self.cbModel.currentText()
2420
2421            # reread the category file and repopulate the combo
2422            self.cbCategory.blockSignals(True)
2423            self.cbCategory.clear()
2424            self.readCategoryInfo()
2425            self.initializeCategoryCombo()
2426
2427            # Scroll back to the original index in Categories
2428            new_index = self.cbCategory.findText(current_cat)
2429            if new_index != -1:
2430                self.cbCategory.setCurrentIndex(new_index)
2431            self.cbCategory.blockSignals(False)
2432            # ...and in the Models
2433            self.cbModel.blockSignals(True)
2434            new_index = self.cbModel.findText(current_model)
2435            if new_index != -1:
2436                self.cbModel.setCurrentIndex(new_index)
2437            self.cbModel.blockSignals(False)
2438
2439            return
2440
2441    def calcException(self, etype, value, tb):
2442        """
2443        Thread threw an exception.
2444        """
2445        # TODO: remimplement thread cancellation
2446        logging.error("".join(traceback.format_exception(etype, value, tb)))
2447
2448    def setTableProperties(self, table):
2449        """
2450        Setting table properties
2451        """
2452        # Table properties
2453        table.verticalHeader().setVisible(False)
2454        table.setAlternatingRowColors(True)
2455        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2456        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
2457        table.resizeColumnsToContents()
2458
2459        # Header
2460        header = table.horizontalHeader()
2461        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2462        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
2463
2464        # Qt5: the following 2 lines crash - figure out why!
2465        # Resize column 0 and 7 to content
2466        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2467        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
2468
2469    def setPolyModel(self):
2470        """
2471        Set polydispersity values
2472        """
2473        if not self.model_parameters:
2474            return
2475        self._poly_model.clear()
2476
2477        parameters = self.model_parameters.form_volume_parameters
2478        if self.is2D:
2479            parameters += self.model_parameters.orientation_parameters
2480
2481        [self.setPolyModelParameters(i, param) for i, param in \
2482            enumerate(parameters) if param.polydisperse]
2483
2484        FittingUtilities.addPolyHeadersToModel(self._poly_model)
2485
2486    def setPolyModelParameters(self, i, param):
2487        """
2488        Standard of multishell poly parameter driver
2489        """
2490        param_name = param.name
2491        # see it the parameter is multishell
2492        if '[' in param.name:
2493            # Skip empty shells
2494            if self.current_shell_displayed == 0:
2495                return
2496            else:
2497                # Create as many entries as current shells
2498                for ishell in range(1, self.current_shell_displayed+1):
2499                    # Remove [n] and add the shell numeral
2500                    name = param_name[0:param_name.index('[')] + str(ishell)
2501                    self.addNameToPolyModel(i, name)
2502        else:
2503            # Just create a simple param entry
2504            self.addNameToPolyModel(i, param_name)
2505
2506    def addNameToPolyModel(self, i, param_name):
2507        """
2508        Creates a checked row in the poly model with param_name
2509        """
2510        # Polydisp. values from the sasmodel
2511        width = self.kernel_module.getParam(param_name + '.width')
2512        npts = self.kernel_module.getParam(param_name + '.npts')
2513        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2514        _, min, max = self.kernel_module.details[param_name]
2515
2516        # Update local param dict
2517        self.poly_params[param_name + '.width'] = width
2518        self.poly_params[param_name + '.npts'] = npts
2519        self.poly_params[param_name + '.nsigmas'] = nsigs
2520
2521        # Construct a row with polydisp. related variable.
2522        # This will get added to the polydisp. model
2523        # Note: last argument needs extra space padding for decent display of the control
2524        checked_list = ["Distribution of " + param_name, str(width),
2525                        str(min), str(max),
2526                        str(npts), str(nsigs), "gaussian      ",'']
2527        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2528
2529        # All possible polydisp. functions as strings in combobox
2530        func = QtWidgets.QComboBox()
2531        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
2532        # Set the default index
2533        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
2534        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2535        self.lstPoly.setIndexWidget(ind, func)
2536        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2537
2538    def onPolyFilenameChange(self, row_index):
2539        """
2540        Respond to filename_updated signal from the delegate
2541        """
2542        # For the given row, invoke the "array" combo handler
2543        array_caption = 'array'
2544
2545        # Get the combo box reference
2546        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2547        widget = self.lstPoly.indexWidget(ind)
2548
2549        # Update the combo box so it displays "array"
2550        widget.blockSignals(True)
2551        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
2552        widget.blockSignals(False)
2553
2554        # Invoke the file reader
2555        self.onPolyComboIndexChange(array_caption, row_index)
2556
2557    def onPolyComboIndexChange(self, combo_string, row_index):
2558        """
2559        Modify polydisp. defaults on function choice
2560        """
2561        # Get npts/nsigs for current selection
2562        param = self.model_parameters.form_volume_parameters[row_index]
2563        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2564        combo_box = self.lstPoly.indexWidget(file_index)
2565
2566        def updateFunctionCaption(row):
2567            # Utility function for update of polydispersity function name in the main model
2568            self._model_model.blockSignals(True)
2569            param_name = str(self._model_model.item(row, 0).text())
2570            self._model_model.blockSignals(False)
2571            if param_name !=  param.name:
2572                return
2573            # Modify the param value
2574            self._model_model.blockSignals(True)
2575            if self.has_error_column:
2576                # err column changes the indexing
2577                self._model_model.item(row, 0).child(0).child(0,5).setText(combo_string)
2578            else:
2579                self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
2580            self._model_model.blockSignals(False)
2581
2582        if combo_string == 'array':
2583            try:
2584                self.loadPolydispArray(row_index)
2585                # Update main model for display
2586                self.iterateOverModel(updateFunctionCaption)
2587                # disable the row
2588                lo = self.lstPoly.itemDelegate().poly_pd
2589                hi = self.lstPoly.itemDelegate().poly_function
2590                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
2591                return
2592            except IOError:
2593                combo_box.setCurrentIndex(self.orig_poly_index)
2594                # Pass for cancel/bad read
2595                pass
2596
2597        # Enable the row in case it was disabled by Array
2598        self._poly_model.blockSignals(True)
2599        max_range = self.lstPoly.itemDelegate().poly_filename
2600        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
2601        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2602        self._poly_model.setData(file_index, "")
2603        self._poly_model.blockSignals(False)
2604
2605        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
2606        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
2607
2608        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
2609        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
2610
2611        self._poly_model.setData(npts_index, npts)
2612        self._poly_model.setData(nsigs_index, nsigs)
2613
2614        self.iterateOverModel(updateFunctionCaption)
2615        self.orig_poly_index = combo_box.currentIndex()
2616
2617    def loadPolydispArray(self, row_index):
2618        """
2619        Show the load file dialog and loads requested data into state
2620        """
2621        datafile = QtWidgets.QFileDialog.getOpenFileName(
2622            self, "Choose a weight file", "", "All files (*.*)", None,
2623            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
2624
2625        if not datafile:
2626            logging.info("No weight data chosen.")
2627            raise IOError
2628
2629        values = []
2630        weights = []
2631        def appendData(data_tuple):
2632            """
2633            Fish out floats from a tuple of strings
2634            """
2635            try:
2636                values.append(float(data_tuple[0]))
2637                weights.append(float(data_tuple[1]))
2638            except (ValueError, IndexError):
2639                # just pass through if line with bad data
2640                return
2641
2642        with open(datafile, 'r') as column_file:
2643            column_data = [line.rstrip().split() for line in column_file.readlines()]
2644            [appendData(line) for line in column_data]
2645
2646        # If everything went well - update the sasmodel values
2647        self.disp_model = POLYDISPERSITY_MODELS['array']()
2648        self.disp_model.set_weights(np.array(values), np.array(weights))
2649        # + update the cell with filename
2650        fname = os.path.basename(str(datafile))
2651        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2652        self._poly_model.setData(fname_index, fname)
2653
2654    def setMagneticModel(self):
2655        """
2656        Set magnetism values on model
2657        """
2658        if not self.model_parameters:
2659            return
2660        self._magnet_model.clear()
2661        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
2662            self.model_parameters.call_parameters if param.type == 'magnetic']
2663        FittingUtilities.addHeadersToModel(self._magnet_model)
2664
2665    def shellNamesList(self):
2666        """
2667        Returns list of names of all multi-shell parameters
2668        E.g. for sld[n], radius[n], n=1..3 it will return
2669        [sld1, sld2, sld3, radius1, radius2, radius3]
2670        """
2671        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
2672        top_index = self.kernel_module.multiplicity_info.number
2673        shell_names = []
2674        for i in range(1, top_index+1):
2675            for name in multi_names:
2676                shell_names.append(name+str(i))
2677        return shell_names
2678
2679    def addCheckedMagneticListToModel(self, param, model):
2680        """
2681        Wrapper for model update with a subset of magnetic parameters
2682        """
2683        if param.name[param.name.index(':')+1:] in self.shell_names:
2684            # check if two-digit shell number
2685            try:
2686                shell_index = int(param.name[-2:])
2687            except ValueError:
2688                shell_index = int(param.name[-1:])
2689
2690            if shell_index > self.current_shell_displayed:
2691                return
2692
2693        checked_list = [param.name,
2694                        str(param.default),
2695                        str(param.limits[0]),
2696                        str(param.limits[1]),
2697                        param.units]
2698
2699        self.magnet_params[param.name] = param.default
2700
2701        FittingUtilities.addCheckedListToModel(model, checked_list)
2702
2703    def enableStructureFactorControl(self, structure_factor):
2704        """
2705        Add structure factors to the list of parameters
2706        """
2707        if self.kernel_module.is_form_factor or structure_factor == 'None':
2708            self.enableStructureCombo()
2709        else:
2710            self.disableStructureCombo()
2711
2712    def addExtraShells(self):
2713        """
2714        Add a combobox for multiple shell display
2715        """
2716        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
2717
2718        if param_length == 0:
2719            return
2720
2721        # cell 1: variable name
2722        item1 = QtGui.QStandardItem(param_name)
2723
2724        func = QtWidgets.QComboBox()
2725        # Available range of shells displayed in the combobox
2726        func.addItems([str(i) for i in range(param_length+1)])
2727
2728        # Respond to index change
2729        func.currentIndexChanged.connect(self.modifyShellsInList)
2730
2731        # cell 2: combobox
2732        item2 = QtGui.QStandardItem()
2733        self._model_model.appendRow([item1, item2])
2734
2735        # Beautify the row:  span columns 2-4
2736        shell_row = self._model_model.rowCount()
2737        shell_index = self._model_model.index(shell_row-1, 1)
2738
2739        self.lstParams.setIndexWidget(shell_index, func)
2740        self._last_model_row = self._model_model.rowCount()
2741
2742        # Set the index to the state-kept value
2743        func.setCurrentIndex(self.current_shell_displayed
2744                             if self.current_shell_displayed < func.count() else 0)
2745
2746    def modifyShellsInList(self, index):
2747        """
2748        Add/remove additional multishell parameters
2749        """
2750        # Find row location of the combobox
2751        last_row = self._last_model_row
2752        remove_rows = self._model_model.rowCount() - last_row
2753
2754        if remove_rows > 1:
2755            self._model_model.removeRows(last_row, remove_rows)
2756
2757        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
2758        self.current_shell_displayed = index
2759
2760        # Update relevant models
2761        self.setPolyModel()
2762        self.setMagneticModel()
2763
2764    def setFittingStarted(self):
2765        """
2766        Set buttion caption on fitting start
2767        """
2768        # Notify the user that fitting is being run
2769        # Allow for stopping the job
2770        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
2771        self.cmdFit.setText('Stop fit')
2772
2773    def setFittingStopped(self):
2774        """
2775        Set button caption on fitting stop
2776        """
2777        # Notify the user that fitting is available
2778        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
2779        self.cmdFit.setText("Fit")
2780        self.fit_started = False
2781
2782    def readFitPage(self, fp):
2783        """
2784        Read in state from a fitpage object and update GUI
2785        """
2786        assert isinstance(fp, FitPage)
2787        # Main tab info
2788        self.logic.data.filename = fp.filename
2789        self.data_is_loaded = fp.data_is_loaded
2790        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
2791        self.chkMagnetism.setCheckState(fp.is_magnetic)
2792        self.chk2DView.setCheckState(fp.is2D)
2793
2794        # Update the comboboxes
2795        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
2796        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
2797        if fp.current_factor:
2798            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
2799
2800        self.chi2 = fp.chi2
2801
2802        # Options tab
2803        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
2804        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
2805        self.npts = fp.fit_options[fp.NPTS]
2806        self.log_points = fp.fit_options[fp.LOG_POINTS]
2807        self.weighting = fp.fit_options[fp.WEIGHTING]
2808
2809        # Models
2810        self._model_model = fp.model_model
2811        self._poly_model = fp.poly_model
2812        self._magnet_model = fp.magnetism_model
2813
2814        # Resolution tab
2815        smearing = fp.smearing_options[fp.SMEARING_OPTION]
2816        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
2817        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
2818        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
2819        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
2820
2821        # TODO: add polidyspersity and magnetism
2822
2823    def saveToFitPage(self, fp):
2824        """
2825        Write current state to the given fitpage
2826        """
2827        assert isinstance(fp, FitPage)
2828
2829        # Main tab info
2830        fp.filename = self.logic.data.filename
2831        fp.data_is_loaded = self.data_is_loaded
2832        fp.is_polydisperse = self.chkPolydispersity.isChecked()
2833        fp.is_magnetic = self.chkMagnetism.isChecked()
2834        fp.is2D = self.chk2DView.isChecked()
2835        fp.data = self.data
2836
2837        # Use current models - they contain all the required parameters
2838        fp.model_model = self._model_model
2839        fp.poly_model = self._poly_model
2840        fp.magnetism_model = self._magnet_model
2841
2842        if self.cbCategory.currentIndex() != 0:
2843            fp.current_category = str(self.cbCategory.currentText())
2844            fp.current_model = str(self.cbModel.currentText())
2845
2846        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
2847            fp.current_factor = str(self.cbStructureFactor.currentText())
2848        else:
2849            fp.current_factor = ''
2850
2851        fp.chi2 = self.chi2
2852        fp.main_params_to_fit = self.main_params_to_fit
2853        fp.poly_params_to_fit = self.poly_params_to_fit
2854        fp.magnet_params_to_fit = self.magnet_params_to_fit
2855        fp.kernel_module = self.kernel_module
2856
2857        # Algorithm options
2858        # fp.algorithm = self.parent.fit_options.selected_id
2859
2860        # Options tab
2861        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
2862        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
2863        fp.fit_options[fp.NPTS] = self.npts
2864        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
2865        fp.fit_options[fp.LOG_POINTS] = self.log_points
2866        fp.fit_options[fp.WEIGHTING] = self.weighting
2867
2868        # Resolution tab
2869        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
2870        fp.smearing_options[fp.SMEARING_OPTION] = smearing
2871        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
2872        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
2873        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
2874
2875        # TODO: add polidyspersity and magnetism
2876
2877    def updateUndo(self):
2878        """
2879        Create a new state page and add it to the stack
2880        """
2881        if self.undo_supported:
2882            self.pushFitPage(self.currentState())
2883
2884    def currentState(self):
2885        """
2886        Return fit page with current state
2887        """
2888        new_page = FitPage()
2889        self.saveToFitPage(new_page)
2890
2891        return new_page
2892
2893    def pushFitPage(self, new_page):
2894        """
2895        Add a new fit page object with current state
2896        """
2897        self.page_stack.append(new_page)
2898
2899    def popFitPage(self):
2900        """
2901        Remove top fit page from stack
2902        """
2903        if self.page_stack:
2904            self.page_stack.pop()
2905
2906    def getReport(self):
2907        """
2908        Create and return HTML report with parameters and charts
2909        """
2910        index = None
2911        if self.all_data:
2912            index = self.all_data[self.data_index]
2913        else:
2914            index = self.theory_item
2915        report_logic = ReportPageLogic(self,
2916                                       kernel_module=self.kernel_module,
2917                                       data=self.data,
2918                                       index=index,
2919                                       model=self._model_model)
2920
2921        return report_logic.reportList()
2922
2923    def savePageState(self):
2924        """
2925        Create and serialize local PageState
2926        """
2927        from sas.sascalc.fit.pagestate import Reader
2928        model = self.kernel_module
2929
2930        # Old style PageState object
2931        state = PageState(model=model, data=self.data)
2932
2933        # Add parameter data to the state
2934        self.getCurrentFitState(state)
2935
2936        # Create the filewriter, aptly named 'Reader'
2937        state_reader = Reader(self.loadPageStateCallback)
2938        filepath = self.saveAsAnalysisFile()
2939        if filepath is None or filepath == "":
2940            return
2941        state_reader.write(filename=filepath, fitstate=state)
2942        pass
2943
2944    def saveAsAnalysisFile(self):
2945        """
2946        Show the save as... dialog and return the chosen filepath
2947        """
2948        default_name = "FitPage"+str(self.tab_id)+".fitv"
2949
2950        wildcard = "fitv files (*.fitv)"
2951        kwargs = {
2952            'caption'   : 'Save As',
2953            'directory' : default_name,
2954            'filter'    : wildcard,
2955            'parent'    : None,
2956        }
2957        # Query user for filename.
2958        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
2959        filename = filename_tuple[0]
2960        return filename
2961
2962    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
2963        """
2964        This is a callback method called from the CANSAS reader.
2965        We need the instance of this reader only for writing out a file,
2966        so there's nothing here.
2967        Until Load Analysis is implemented, that is.
2968        """
2969        pass
2970
2971    def loadPageState(self, pagestate=None):
2972        """
2973        Load the PageState object and update the current widget
2974        """
2975        pass
2976
2977    def getCurrentFitState(self, state=None):
2978        """
2979        Store current state for fit_page
2980        """
2981        # save model option
2982        #if self.model is not None:
2983        #    self.disp_list = self.getDispParamList()
2984        #    state.disp_list = copy.deepcopy(self.disp_list)
2985        #    #state.model = self.model.clone()
2986
2987        # Comboboxes
2988        state.categorycombobox = self.cbCategory.currentText()
2989        state.formfactorcombobox = self.cbModel.currentText()
2990        if self.cbStructureFactor.isEnabled():
2991            state.structurecombobox = self.cbStructureFactor.currentText()
2992        state.tcChi = self.chi2
2993
2994        state.enable2D = self.is2D
2995
2996        #state.weights = copy.deepcopy(self.weights)
2997        # save data
2998        state.data = copy.deepcopy(self.data)
2999
3000        # save plotting range
3001        state.qmin = self.q_range_min
3002        state.qmax = self.q_range_max
3003        state.npts = self.npts
3004
3005        #    self.state.enable_disp = self.enable_disp.GetValue()
3006        #    self.state.disable_disp = self.disable_disp.GetValue()
3007
3008        #    self.state.enable_smearer = \
3009        #                        copy.deepcopy(self.enable_smearer.GetValue())
3010        #    self.state.disable_smearer = \
3011        #                        copy.deepcopy(self.disable_smearer.GetValue())
3012
3013        #self.state.pinhole_smearer = \
3014        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
3015        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
3016        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
3017        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
3018        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
3019        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
3020
3021        p = self.model_parameters
3022        # save checkbutton state and txtcrtl values
3023        state.parameters = FittingUtilities.getStandardParam(self._model_model)
3024        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
3025
3026        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
3027        #self._copy_parameters_state(self.parameters, self.state.parameters)
3028        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
3029        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
3030
3031    def onParameterCopy(self, format=None):
3032        """
3033        Copy current parameters into the clipboard
3034        """
3035        # run a loop over all parameters and pull out
3036        # first - regular params
3037        param_list = []
3038
3039        param_list.append(['model_name', str(self.cbModel.currentText())])
3040        def gatherParams(row):
3041            """
3042            Create list of main parameters based on _model_model
3043            """
3044            param_name = str(self._model_model.item(row, 0).text())
3045            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3046            param_value = str(self._model_model.item(row, 1).text())
3047            param_error = None
3048            column_offset = 0
3049            if self.has_error_column:
3050                param_error = str(self._model_model.item(row, 2).text())
3051                column_offset = 1
3052            param_min = str(self._model_model.item(row, 2+column_offset).text())
3053            param_max = str(self._model_model.item(row, 3+column_offset).text())
3054            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3055
3056        def gatherPolyParams(row):
3057            """
3058            Create list of polydisperse parameters based on _poly_model
3059            """
3060            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
3061            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3062            param_value = str(self._poly_model.item(row, 1).text())
3063            param_error = None
3064            column_offset = 0
3065            if self.has_poly_error_column:
3066                param_error = str(self._poly_model.item(row, 2).text())
3067                column_offset = 1
3068            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
3069            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
3070            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
3071            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
3072            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
3073            # width
3074            name = param_name+".width"
3075            param_list.append([name, param_checked, param_value, param_error,
3076                                param_npts, param_nsigs, param_min, param_max, param_fun])
3077
3078        def gatherMagnetParams(row):
3079            """
3080            Create list of magnetic parameters based on _magnet_model
3081            """
3082            param_name = str(self._magnet_model.item(row, 0).text())
3083            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3084            param_value = str(self._magnet_model.item(row, 1).text())
3085            param_error = None
3086            column_offset = 0
3087            if self.has_magnet_error_column:
3088                param_error = str(self._magnet_model.item(row, 2).text())
3089                column_offset = 1
3090            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
3091            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
3092            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3093
3094        self.iterateOverModel(gatherParams)
3095        if self.chkPolydispersity.isChecked():
3096            self.iterateOverPolyModel(gatherPolyParams)
3097        if self.chkMagnetism.isChecked() and self.chkMagnetism.isEnabled():
3098            self.iterateOverMagnetModel(gatherMagnetParams)
3099
3100        if format=="":
3101            formatted_output = FittingUtilities.formatParameters(param_list)
3102        elif format == "Excel":
3103            formatted_output = FittingUtilities.formatParametersExcel(param_list)
3104        elif format == "Latex":
3105            formatted_output = FittingUtilities.formatParametersLatex(param_list)
3106        else:
3107            raise AttributeError("Bad format specifier.")
3108
3109        # Dump formatted_output to the clipboard
3110        cb = QtWidgets.QApplication.clipboard()
3111        cb.setText(formatted_output)
3112
3113    def onParameterPaste(self):
3114        """
3115        Use the clipboard to update fit state
3116        """
3117        # Check if the clipboard contains right stuff
3118        cb = QtWidgets.QApplication.clipboard()
3119        cb_text = cb.text()
3120
3121        context = {}
3122        # put the text into dictionary
3123        lines = cb_text.split(':')
3124        if lines[0] != 'sasview_parameter_values':
3125            return False
3126
3127        model = lines[1].split(',')
3128
3129        if model[0] != 'model_name':
3130            return False
3131
3132        context['model_name'] = [model[1]]
3133        for line in lines[2:-1]:
3134            if len(line) != 0:
3135                item = line.split(',')
3136                check = item[1]
3137                name = item[0]
3138                value = item[2]
3139                # Transfer the text to content[dictionary]
3140                context[name] = [check, value]
3141
3142                # limits
3143                limit_lo = item[3]
3144                context[name].append(limit_lo)
3145                limit_hi = item[4]
3146                context[name].append(limit_hi)
3147
3148                # Polydisp
3149                if len(item) > 5:
3150                    value = item[5]
3151                    context[name].append(value)
3152                    try:
3153                        value = item[6]
3154                        context[name].append(value)
3155                        value = item[7]
3156                        context[name].append(value)
3157                    except IndexError:
3158                        pass
3159
3160        if str(self.cbModel.currentText()) != str(context['model_name'][0]):
3161            msg = QtWidgets.QMessageBox()
3162            msg.setIcon(QtWidgets.QMessageBox.Information)
3163            msg.setText("The model in the clipboard is not the same as the currently loaded model. \
3164                         Not all parameters saved may paste correctly.")
3165            msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
3166            result = msg.exec_()
3167            if result == QtWidgets.QMessageBox.Ok:
3168                pass
3169            else:
3170                return
3171
3172        self.updateFullModel(context)
3173        self.updateFullPolyModel(context)
3174
3175    def updateFullModel(self, param_dict):
3176        """
3177        Update the model with new parameters
3178        """
3179        assert isinstance(param_dict, dict)
3180        if not dict:
3181            return
3182
3183        def updateFittedValues(row):
3184            # Utility function for main model update
3185            # internal so can use closure for param_dict
3186            param_name = str(self._model_model.item(row, 0).text())
3187            if param_name not in list(param_dict.keys()):
3188                return
3189            # checkbox state
3190            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3191            self._model_model.item(row, 0).setCheckState(param_checked)
3192
3193            # modify the param value
3194            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3195            self._model_model.item(row, 1).setText(param_repr)
3196
3197            # Potentially the error column
3198            ioffset = 0
3199            if len(param_dict[param_name])>4 and self.has_error_column:
3200                # error values are not editable - no need to update
3201                #error_repr = GuiUtils.formatNumber(param_dict[param_name][2], high=True)
3202                #self._model_model.item(row, 2).setText(error_repr)
3203                ioffset = 1
3204            # min/max
3205            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3206            self._model_model.item(row, 2+ioffset).setText(param_repr)
3207            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3208            self._model_model.item(row, 3+ioffset).setText(param_repr)
3209            self.setFocus()
3210
3211
3212        # block signals temporarily, so we don't end up
3213        # updating charts with every single model change on the end of fitting
3214        self._model_model.blockSignals(True)
3215        self.iterateOverModel(updateFittedValues)
3216        self._model_model.blockSignals(False)
3217
3218
3219    def updateFullPolyModel(self, param_dict):
3220        """
3221        Update the polydispersity model with new parameters, create the errors column
3222        """
3223        assert isinstance(param_dict, dict)
3224        if not dict:
3225            return
3226
3227        def updateFittedValues(row):
3228            # Utility function for main model update
3229            # internal so can use closure for param_dict
3230            if row >= self._poly_model.rowCount():
3231                return
3232            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
3233            if param_name not in list(param_dict.keys()):
3234                return
3235            # checkbox state
3236            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3237            self._poly_model.item(row,0).setCheckState(param_checked)
3238
3239            # modify the param value
3240            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3241            self._poly_model.item(row, 1).setText(param_repr)
3242
3243            # Potentially the error column
3244            ioffset = 0
3245            if len(param_dict[param_name])>4 and self.has_poly_error_column:
3246                ioffset = 1
3247            # min
3248            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3249            self._poly_model.item(row, 2+ioffset).setText(param_repr)
3250            # max
3251            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3252            self._poly_model.item(row, 3+ioffset).setText(param_repr)
3253            # Npts
3254            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
3255            self._poly_model.item(row, 4+ioffset).setText(param_repr)
3256            # Nsigs
3257            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3258            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3259
3260            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3261            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3262            self.setFocus()
3263
3264        # block signals temporarily, so we don't end up
3265        # updating charts with every single model change on the end of fitting
3266        self._poly_model.blockSignals(True)
3267        self.iterateOverPolyModel(updateFittedValues)
3268        self._poly_model.blockSignals(False)
3269
Note: See TracBrowser for help on using the repository browser.