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

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 50cafe7 was 50cafe7, checked in by Piotr Rozyczko <rozyczko@…>, 8 months ago

Don't show constraint menu on parameter headers.

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