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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since a2b8607 was a2b8607, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Use different message on the Fit button when performing simple model
update. SASVIEW-1177

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