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

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 f3a19ad was f3a19ad, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 2 years ago

refactor & fix param handling; test fixes

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