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

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 c4b23dd was c4b23dd, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 13 months ago

reduce nesting in some FittingWidget? methods

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