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

Last change on this file since 60a4e71 was 60a4e71, checked in by GitHub <noreply@…>, 6 years ago

Merge 8e674ccf5c2f4e357295344dc59f2796cd7de805 into 343d7fd60f1eda3abfbbd120c048168e649876be

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