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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 33d5956 was 33d5956, checked in by ibressler, 6 years ago

FittingWidget?.updateData(): ensure theory is recalc. before plot

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