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

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

Merge branch 'ESS_GUI' into ESS_GUI_iss1034

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