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

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

Batch results tabs now have meaningful names SASVIEW-1204

  • Property mode set to 100644
File size: 152.5 KB
Line 
1import json
2import os
3from collections import defaultdict
4
5import copy
6import logging
7import traceback
8from twisted.internet import threads
9import numpy as np
10import webbrowser
11
12from PyQt5 import QtCore
13from PyQt5 import QtGui
14from PyQt5 import QtWidgets
15
16from sasmodels import generate
17from sasmodels import modelinfo
18from sasmodels.sasview_model import load_standard_models
19from sasmodels.sasview_model import MultiplicationModel
20from sasmodels.weights import MODELS as POLYDISPERSITY_MODELS
21
22from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
23from sas.sascalc.fit.pagestate import PageState
24
25import sas.qtgui.Utilities.GuiUtils as GuiUtils
26import sas.qtgui.Utilities.LocalConfig as LocalConfig
27from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
28from sas.qtgui.Plotting.PlotterData import Data1D
29from sas.qtgui.Plotting.PlotterData import Data2D
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        page_name = "BatchPage" + str(self.tab_id)
1496        results = copy.deepcopy(result[0])
1497        results.append(page_name)
1498        self.communicate.sendDataToGridSignal.emit(results)
1499
1500        elapsed = result[1]
1501        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
1502        self.communicate.statusBarUpdateSignal.emit(msg)
1503
1504        # Run over the list of results and update the items
1505        for res_index, res_list in enumerate(result[0]):
1506            # results
1507            res = res_list[0]
1508            param_dict = self.paramDictFromResults(res)
1509
1510            # create local kernel_module
1511            kernel_module = FittingUtilities.updateKernelWithResults(self.kernel_module, param_dict)
1512            # pull out current data
1513            data = self._logic[res_index].data
1514
1515            # Switch indexes
1516            self.onSelectBatchFilename(res_index)
1517
1518            method = self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1519            self.calculateQGridForModelExt(data=data, model=kernel_module, completefn=method, use_threads=False)
1520
1521        # Restore original kernel_module, so subsequent fits on the same model don't pick up the new params
1522        if self.kernel_module is not None:
1523            self.kernel_module = copy.deepcopy(self.kernel_module_copy)
1524
1525    def paramDictFromResults(self, results):
1526        """
1527        Given the fit results structure, pull out optimized parameters and return them as nicely
1528        formatted dict
1529        """
1530        if results.fitness is None or \
1531            not np.isfinite(results.fitness) or \
1532            np.any(results.pvec is None) or \
1533            not np.all(np.isfinite(results.pvec)):
1534            msg = "Fitting did not converge!"
1535            self.communicate.statusBarUpdateSignal.emit(msg)
1536            msg += results.mesg
1537            logger.error(msg)
1538            return
1539
1540        param_list = results.param_list # ['radius', 'radius.width']
1541        param_values = results.pvec     # array([ 0.36221662,  0.0146783 ])
1542        param_stderr = results.stderr   # array([ 1.71293015,  1.71294233])
1543        params_and_errors = list(zip(param_values, param_stderr))
1544        param_dict = dict(zip(param_list, params_and_errors))
1545
1546        return param_dict
1547
1548    def fittingCompleted(self, result):
1549        """
1550        Send the finish message from calculate threads to main thread
1551        """
1552        if result is None:
1553            result = tuple()
1554        self.fittingFinishedSignal.emit(result)
1555
1556    def fitComplete(self, result):
1557        """
1558        Receive and display fitting results
1559        "result" is a tuple of actual result list and the fit time in seconds
1560        """
1561        #re-enable the Fit button
1562        self.enableInteractiveElements()
1563
1564        if len(result) == 0:
1565            msg = "Fitting failed."
1566            self.communicate.statusBarUpdateSignal.emit(msg)
1567            return
1568
1569        res_list = result[0][0]
1570        res = res_list[0]
1571        self.chi2 = res.fitness
1572        param_dict = self.paramDictFromResults(res)
1573
1574        if param_dict is None:
1575            return
1576        self.communicate.resultPlotUpdateSignal.emit(result[0])
1577
1578        elapsed = result[1]
1579        if self.calc_fit is not None and self.calc_fit._interrupting:
1580            msg = "Fitting cancelled by user after: %s s." % GuiUtils.formatNumber(elapsed)
1581            logger.warning("\n"+msg+"\n")
1582        else:
1583            msg = "Fitting completed successfully in: %s s." % GuiUtils.formatNumber(elapsed)
1584        self.communicate.statusBarUpdateSignal.emit(msg)
1585
1586        # Dictionary of fitted parameter: value, error
1587        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
1588        self.updateModelFromList(param_dict)
1589
1590        self.updatePolyModelFromList(param_dict)
1591
1592        self.updateMagnetModelFromList(param_dict)
1593
1594        # update charts
1595        self.onPlot()
1596        #self.recalculatePlotData()
1597
1598
1599        # Read only value - we can get away by just printing it here
1600        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
1601        self.lblChi2Value.setText(chi2_repr)
1602
1603    def prepareFitters(self, fitter=None, fit_id=0):
1604        """
1605        Prepare the Fitter object for use in fitting
1606        """
1607        # fitter = None -> single/batch fitting
1608        # fitter = Fit() -> simultaneous fitting
1609
1610        # Data going in
1611        data = self.logic.data
1612        model = copy.deepcopy(self.kernel_module)
1613        qmin = self.q_range_min
1614        qmax = self.q_range_max
1615        # add polydisperse/magnet parameters if asked
1616        self.updateKernelModelWithExtraParams(model)
1617
1618        params_to_fit = copy.deepcopy(self.main_params_to_fit)
1619        if self.chkPolydispersity.isChecked():
1620            params_to_fit += self.poly_params_to_fit
1621        if self.chkMagnetism.isChecked() and self.canHaveMagnetism():
1622            params_to_fit += self.magnet_params_to_fit
1623        if not params_to_fit:
1624            raise ValueError('Fitting requires at least one parameter to optimize.')
1625
1626        # Get the constraints.
1627        constraints = self.getComplexConstraintsForModel()
1628        if fitter is None:
1629            # For single fits - check for inter-model constraints
1630            constraints = self.getConstraintsForFitting()
1631
1632        smearer = self.smearing_widget.smearer()
1633        handler = None
1634        batch_inputs = {}
1635        batch_outputs = {}
1636
1637        fitters = []
1638        for fit_index in self.all_data:
1639            fitter_single = Fit() if fitter is None else fitter
1640            data = GuiUtils.dataFromItem(fit_index)
1641            # Potential weights added directly to data
1642            weighted_data = self.addWeightingToData(data)
1643            try:
1644                fitter_single.set_model(model, fit_id, params_to_fit, data=weighted_data,
1645                             constraints=constraints)
1646            except ValueError as ex:
1647                raise ValueError("Setting model parameters failed with: %s" % ex)
1648
1649            qmin, qmax, _ = self.logic.computeRangeFromData(weighted_data)
1650            fitter_single.set_data(data=weighted_data, id=fit_id, smearer=smearer, qmin=qmin,
1651                            qmax=qmax)
1652            fitter_single.select_problem_for_fit(id=fit_id, value=1)
1653            if fitter is None:
1654                # Assign id to the new fitter only
1655                fitter_single.fitter_id = [self.page_id]
1656            fit_id += 1
1657            fitters.append(fitter_single)
1658
1659        return fitters, fit_id
1660
1661    def iterateOverModel(self, func):
1662        """
1663        Take func and throw it inside the model row loop
1664        """
1665        for row_i in range(self._model_model.rowCount()):
1666            func(row_i)
1667
1668    def updateModelFromList(self, param_dict):
1669        """
1670        Update the model with new parameters, create the errors column
1671        """
1672        assert isinstance(param_dict, dict)
1673        if not dict:
1674            return
1675
1676        def updateFittedValues(row):
1677            # Utility function for main model update
1678            # internal so can use closure for param_dict
1679            param_name = str(self._model_model.item(row, 0).text())
1680            if not self.isCheckable(row) or param_name not in list(param_dict.keys()):
1681                return
1682            # modify the param value
1683            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1684            self._model_model.item(row, 1).setText(param_repr)
1685            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1686            if self.has_error_column:
1687                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1688                self._model_model.item(row, 2).setText(error_repr)
1689
1690        def updatePolyValues(row):
1691            # Utility function for updateof polydispersity part of the main model
1692            param_name = str(self._model_model.item(row, 0).text())+'.width'
1693            if not self.isCheckable(row) or param_name not in list(param_dict.keys()):
1694                return
1695            # modify the param value
1696            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1697            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
1698            # modify the param error
1699            if self.has_error_column:
1700                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1701                self._model_model.item(row, 0).child(0).child(0,2).setText(error_repr)
1702
1703        def createErrorColumn(row):
1704            # Utility function for error column update
1705            item = QtGui.QStandardItem()
1706            def createItem(param_name):
1707                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1708                item.setText(error_repr)
1709            def curr_param():
1710                return str(self._model_model.item(row, 0).text())
1711
1712            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1713
1714            error_column.append(item)
1715
1716        def createPolyErrorColumn(row):
1717            # Utility function for error column update in the polydispersity sub-rows
1718            # NOTE: only creates empty items; updatePolyValues adds the error value
1719            item = self._model_model.item(row, 0)
1720            if not item.hasChildren():
1721                return
1722            poly_item = item.child(0)
1723            if not poly_item.hasChildren():
1724                return
1725            poly_item.insertColumn(2, [QtGui.QStandardItem("")])
1726
1727        if not self.has_error_column:
1728            # create top-level error column
1729            error_column = []
1730            self.lstParams.itemDelegate().addErrorColumn()
1731            self.iterateOverModel(createErrorColumn)
1732
1733            self._model_model.insertColumn(2, error_column)
1734
1735            FittingUtilities.addErrorHeadersToModel(self._model_model)
1736
1737            # create error column in polydispersity sub-rows
1738            self.iterateOverModel(createPolyErrorColumn)
1739
1740            self.has_error_column = True
1741
1742        # block signals temporarily, so we don't end up
1743        # updating charts with every single model change on the end of fitting
1744        self._model_model.dataChanged.disconnect()
1745        self.iterateOverModel(updateFittedValues)
1746        self.iterateOverModel(updatePolyValues)
1747        self._model_model.dataChanged.connect(self.onMainParamsChange)
1748
1749    def iterateOverPolyModel(self, func):
1750        """
1751        Take func and throw it inside the poly model row loop
1752        """
1753        for row_i in range(self._poly_model.rowCount()):
1754            func(row_i)
1755
1756    def updatePolyModelFromList(self, param_dict):
1757        """
1758        Update the polydispersity model with new parameters, create the errors column
1759        """
1760        assert isinstance(param_dict, dict)
1761        if not dict:
1762            return
1763
1764        def updateFittedValues(row_i):
1765            # Utility function for main model update
1766            # internal so can use closure for param_dict
1767            if row_i >= self._poly_model.rowCount():
1768                return
1769            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1770            if param_name not in list(param_dict.keys()):
1771                return
1772            # modify the param value
1773            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1774            self._poly_model.item(row_i, 1).setText(param_repr)
1775            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1776            if self.has_poly_error_column:
1777                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1778                self._poly_model.item(row_i, 2).setText(error_repr)
1779
1780        def createErrorColumn(row_i):
1781            # Utility function for error column update
1782            if row_i >= self._poly_model.rowCount():
1783                return
1784            item = QtGui.QStandardItem()
1785
1786            def createItem(param_name):
1787                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1788                item.setText(error_repr)
1789
1790            def poly_param():
1791                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1792
1793            [createItem(param_name) for param_name in list(param_dict.keys()) if poly_param() == param_name]
1794
1795            error_column.append(item)
1796
1797        # block signals temporarily, so we don't end up
1798        # updating charts with every single model change on the end of fitting
1799        self._poly_model.dataChanged.disconnect()
1800        self.iterateOverPolyModel(updateFittedValues)
1801        self._poly_model.dataChanged.connect(self.onPolyModelChange)
1802
1803        if self.has_poly_error_column:
1804            return
1805
1806        self.lstPoly.itemDelegate().addErrorColumn()
1807        error_column = []
1808        self.iterateOverPolyModel(createErrorColumn)
1809
1810        # switch off reponse to model change
1811        self._poly_model.insertColumn(2, error_column)
1812        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
1813
1814        self.has_poly_error_column = True
1815
1816    def iterateOverMagnetModel(self, func):
1817        """
1818        Take func and throw it inside the magnet model row loop
1819        """
1820        for row_i in range(self._magnet_model.rowCount()):
1821            func(row_i)
1822
1823    def updateMagnetModelFromList(self, param_dict):
1824        """
1825        Update the magnetic model with new parameters, create the errors column
1826        """
1827        assert isinstance(param_dict, dict)
1828        if not dict:
1829            return
1830        if self._magnet_model.rowCount() == 0:
1831            return
1832
1833        def updateFittedValues(row):
1834            # Utility function for main model update
1835            # internal so can use closure for param_dict
1836            if self._magnet_model.item(row, 0) is None:
1837                return
1838            param_name = str(self._magnet_model.item(row, 0).text())
1839            if param_name not in list(param_dict.keys()):
1840                return
1841            # modify the param value
1842            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1843            self._magnet_model.item(row, 1).setText(param_repr)
1844            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1845            if self.has_magnet_error_column:
1846                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1847                self._magnet_model.item(row, 2).setText(error_repr)
1848
1849        def createErrorColumn(row):
1850            # Utility function for error column update
1851            item = QtGui.QStandardItem()
1852            def createItem(param_name):
1853                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1854                item.setText(error_repr)
1855            def curr_param():
1856                return str(self._magnet_model.item(row, 0).text())
1857
1858            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1859
1860            error_column.append(item)
1861
1862        # block signals temporarily, so we don't end up
1863        # updating charts with every single model change on the end of fitting
1864        self._magnet_model.dataChanged.disconnect()
1865        self.iterateOverMagnetModel(updateFittedValues)
1866        self._magnet_model.dataChanged.connect(self.onMagnetModelChange)
1867
1868        if self.has_magnet_error_column:
1869            return
1870
1871        self.lstMagnetic.itemDelegate().addErrorColumn()
1872        error_column = []
1873        self.iterateOverMagnetModel(createErrorColumn)
1874
1875        # switch off reponse to model change
1876        self._magnet_model.insertColumn(2, error_column)
1877        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1878
1879        self.has_magnet_error_column = True
1880
1881    def onPlot(self):
1882        """
1883        Plot the current set of data
1884        """
1885        # Regardless of previous state, this should now be `plot show` functionality only
1886        self.cmdPlot.setText("Show Plot")
1887        # Force data recalculation so existing charts are updated
1888        if not self.data_is_loaded:
1889            self.showTheoryPlot()
1890        else:
1891            self.showPlot()
1892        # This is an important processEvent.
1893        # This allows charts to be properly updated in order
1894        # of plots being applied.
1895        QtWidgets.QApplication.processEvents()
1896        self.recalculatePlotData() # recalc+plot theory again (2nd)
1897
1898    def onSmearingOptionsUpdate(self):
1899        """
1900        React to changes in the smearing widget
1901        """
1902        self.calculateQGridForModel()
1903
1904    def recalculatePlotData(self):
1905        """
1906        Generate a new dataset for model
1907        """
1908        if not self.data_is_loaded:
1909            self.createDefaultDataset()
1910        self.calculateQGridForModel()
1911
1912    def showTheoryPlot(self):
1913        """
1914        Show the current theory plot in MPL
1915        """
1916        # Show the chart if ready
1917        if self.theory_item is None:
1918            self.recalculatePlotData()
1919        elif self.model_data:
1920            self._requestPlots(self.model_data.filename, self.theory_item.model())
1921
1922    def showPlot(self):
1923        """
1924        Show the current plot in MPL
1925        """
1926        # Show the chart if ready
1927        data_to_show = self.data
1928        # Any models for this page
1929        current_index = self.all_data[self.data_index]
1930        item = self._requestPlots(self.data.filename, current_index.model())
1931        if item:
1932            # fit+data has not been shown - show just data
1933            self.communicate.plotRequestedSignal.emit([item, data_to_show], self.tab_id)
1934
1935    def _requestPlots(self, item_name, item_model):
1936        """
1937        Emits plotRequestedSignal for all plots found in the given model under the provided item name.
1938        """
1939        fitpage_name = "" if self.tab_id is None else "M"+str(self.tab_id)
1940        plots = GuiUtils.plotsFromFilename(item_name, item_model)
1941        # Has the fitted data been shown?
1942        data_shown = False
1943        item = None
1944        for item, plot in plots.items():
1945            if fitpage_name in plot.name:
1946                data_shown = True
1947                self.communicate.plotRequestedSignal.emit([item, plot], self.tab_id)
1948        # return the last data item seen, if nothing was plotted; supposed to be just data)
1949        return None if data_shown else item
1950
1951    def onOptionsUpdate(self):
1952        """
1953        Update local option values and replot
1954        """
1955        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1956            self.options_widget.state()
1957        # set Q range labels on the main tab
1958        self.lblMinRangeDef.setText(GuiUtils.formatNumber(self.q_range_min, high=True))
1959        self.lblMaxRangeDef.setText(GuiUtils.formatNumber(self.q_range_max, high=True))
1960        self.recalculatePlotData()
1961
1962    def setDefaultStructureCombo(self):
1963        """
1964        Fill in the structure factors combo box with defaults
1965        """
1966        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1967        factors = [factor[0] for factor in structure_factor_list]
1968        factors.insert(0, STRUCTURE_DEFAULT)
1969        self.cbStructureFactor.clear()
1970        self.cbStructureFactor.addItems(sorted(factors))
1971
1972    def createDefaultDataset(self):
1973        """
1974        Generate default Dataset 1D/2D for the given model
1975        """
1976        # Create default datasets if no data passed
1977        if self.is2D:
1978            qmax = self.q_range_max/np.sqrt(2)
1979            qstep = self.npts
1980            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
1981            return
1982        elif self.log_points:
1983            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
1984            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
1985            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
1986        else:
1987            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
1988                                   num=self.npts, endpoint=True)
1989        self.logic.createDefault1dData(interval, self.tab_id)
1990
1991    def readCategoryInfo(self):
1992        """
1993        Reads the categories in from file
1994        """
1995        self.master_category_dict = defaultdict(list)
1996        self.by_model_dict = defaultdict(list)
1997        self.model_enabled_dict = defaultdict(bool)
1998
1999        categorization_file = CategoryInstaller.get_user_file()
2000        if not os.path.isfile(categorization_file):
2001            categorization_file = CategoryInstaller.get_default_file()
2002        with open(categorization_file, 'rb') as cat_file:
2003            self.master_category_dict = json.load(cat_file)
2004            self.regenerateModelDict()
2005
2006        # Load the model dict
2007        models = load_standard_models()
2008        for model in models:
2009            self.models[model.name] = model
2010
2011        self.readCustomCategoryInfo()
2012
2013    def readCustomCategoryInfo(self):
2014        """
2015        Reads the custom model category
2016        """
2017        #Looking for plugins
2018        self.plugins = list(self.custom_models.values())
2019        plugin_list = []
2020        for name, plug in self.custom_models.items():
2021            self.models[name] = plug
2022            plugin_list.append([name, True])
2023        if plugin_list:
2024            self.master_category_dict[CATEGORY_CUSTOM] = plugin_list
2025
2026    def regenerateModelDict(self):
2027        """
2028        Regenerates self.by_model_dict which has each model name as the
2029        key and the list of categories belonging to that model
2030        along with the enabled mapping
2031        """
2032        self.by_model_dict = defaultdict(list)
2033        for category in self.master_category_dict:
2034            for (model, enabled) in self.master_category_dict[category]:
2035                self.by_model_dict[model].append(category)
2036                self.model_enabled_dict[model] = enabled
2037
2038    def addBackgroundToModel(self, model):
2039        """
2040        Adds background parameter with default values to the model
2041        """
2042        assert isinstance(model, QtGui.QStandardItemModel)
2043        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
2044        FittingUtilities.addCheckedListToModel(model, checked_list)
2045        last_row = model.rowCount()-1
2046        model.item(last_row, 0).setEditable(False)
2047        model.item(last_row, 4).setEditable(False)
2048
2049    def addScaleToModel(self, model):
2050        """
2051        Adds scale parameter with default values to the model
2052        """
2053        assert isinstance(model, QtGui.QStandardItemModel)
2054        checked_list = ['scale', '1.0', '0.0', 'inf', '']
2055        FittingUtilities.addCheckedListToModel(model, checked_list)
2056        last_row = model.rowCount()-1
2057        model.item(last_row, 0).setEditable(False)
2058        model.item(last_row, 4).setEditable(False)
2059
2060    def addWeightingToData(self, data):
2061        """
2062        Adds weighting contribution to fitting data
2063        """
2064        new_data = copy.deepcopy(data)
2065        # Send original data for weighting
2066        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2067        if self.is2D:
2068            new_data.err_data = weight
2069        else:
2070            new_data.dy = weight
2071
2072        return new_data
2073
2074    def updateQRange(self):
2075        """
2076        Updates Q Range display
2077        """
2078        if self.data_is_loaded:
2079            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
2080        # set Q range labels on the main tab
2081        self.lblMinRangeDef.setText(GuiUtils.formatNumber(self.q_range_min, high=True))
2082        self.lblMaxRangeDef.setText(GuiUtils.formatNumber(self.q_range_max, high=True))
2083        # set Q range labels on the options tab
2084        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
2085
2086    def SASModelToQModel(self, model_name, structure_factor=None):
2087        """
2088        Setting model parameters into table based on selected category
2089        """
2090        # Crete/overwrite model items
2091        self._model_model.clear()
2092        self._poly_model.clear()
2093        self._magnet_model.clear()
2094
2095        if model_name is None:
2096            if structure_factor not in (None, "None"):
2097                # S(Q) on its own, treat the same as a form factor
2098                self.kernel_module = None
2099                self.fromStructureFactorToQModel(structure_factor)
2100            else:
2101                # No models selected
2102                return
2103        else:
2104            self.fromModelToQModel(model_name)
2105            self.addExtraShells()
2106
2107            # Allow the SF combobox visibility for the given sasmodel
2108            self.enableStructureFactorControl(structure_factor)
2109       
2110            # Add S(Q)
2111            if self.cbStructureFactor.isEnabled():
2112                structure_factor = self.cbStructureFactor.currentText()
2113                self.fromStructureFactorToQModel(structure_factor)
2114
2115            # Add polydispersity to the model
2116            self.poly_params = {}
2117            self.setPolyModel()
2118            # Add magnetic parameters to the model
2119            self.magnet_params = {}
2120            self.setMagneticModel()
2121
2122        # Now we claim the model has been loaded
2123        self.model_is_loaded = True
2124        # Change the model name to a monicker
2125        self.kernel_module.name = self.modelName()
2126        # Update the smearing tab
2127        self.smearing_widget.updateKernelModel(kernel_model=self.kernel_module)
2128
2129        # (Re)-create headers
2130        FittingUtilities.addHeadersToModel(self._model_model)
2131        self.lstParams.header().setFont(self.boldFont)
2132
2133        # Update Q Ranges
2134        self.updateQRange()
2135
2136    def fromModelToQModel(self, model_name):
2137        """
2138        Setting model parameters into QStandardItemModel based on selected _model_
2139        """
2140        name = model_name
2141        kernel_module = None
2142        if self.cbCategory.currentText() == CATEGORY_CUSTOM:
2143            # custom kernel load requires full path
2144            name = os.path.join(ModelUtilities.find_plugins_dir(), model_name+".py")
2145        try:
2146            kernel_module = generate.load_kernel_module(name)
2147        except ModuleNotFoundError as ex:
2148            pass
2149        except FileNotFoundError as ex:
2150            # can happen when name attribute not the same as actual filename
2151            pass
2152
2153        if kernel_module is None:
2154            # mismatch between "name" attribute and actual filename.
2155            curr_model = self.models[model_name]
2156            name, _ = os.path.splitext(os.path.basename(curr_model.filename))
2157            try:
2158                kernel_module = generate.load_kernel_module(name)
2159            except ModuleNotFoundError as ex:
2160                logger.error("Can't find the model "+ str(ex))
2161                return
2162
2163        if hasattr(kernel_module, 'parameters'):
2164            # built-in and custom models
2165            self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
2166
2167        elif hasattr(kernel_module, 'model_info'):
2168            # for sum/multiply models
2169            self.model_parameters = kernel_module.model_info.parameters
2170
2171        elif hasattr(kernel_module, 'Model') and hasattr(kernel_module.Model, "_model_info"):
2172            # this probably won't work if there's no model_info, but just in case
2173            self.model_parameters = kernel_module.Model._model_info.parameters
2174        else:
2175            # no parameters - default to blank table
2176            msg = "No parameters found in model '{}'.".format(model_name)
2177            logger.warning(msg)
2178            self.model_parameters = modelinfo.ParameterTable([])
2179
2180        # Instantiate the current sasmodel
2181        self.kernel_module = self.models[model_name]()
2182
2183        # Change the model name to a monicker
2184        self.kernel_module.name = self.modelName()
2185
2186        # Explicitly add scale and background with default values
2187        temp_undo_state = self.undo_supported
2188        self.undo_supported = False
2189        self.addScaleToModel(self._model_model)
2190        self.addBackgroundToModel(self._model_model)
2191        self.undo_supported = temp_undo_state
2192
2193        self.shell_names = self.shellNamesList()
2194
2195        # Add heading row
2196        FittingUtilities.addHeadingRowToModel(self._model_model, model_name)
2197
2198        # Update the QModel
2199        FittingUtilities.addParametersToModel(
2200                self.model_parameters,
2201                self.kernel_module,
2202                self.is2D,
2203                self._model_model,
2204                self.lstParams)
2205
2206    def fromStructureFactorToQModel(self, structure_factor):
2207        """
2208        Setting model parameters into QStandardItemModel based on selected _structure factor_
2209        """
2210        if structure_factor is None or structure_factor=="None":
2211            return
2212
2213        product_params = None
2214
2215        if self.kernel_module is None:
2216            # Structure factor is the only selected model; build it and show all its params
2217            self.kernel_module = self.models[structure_factor]()
2218            self.kernel_module.name = self.modelName()
2219            s_params = self.kernel_module._model_info.parameters
2220            s_params_orig = s_params
2221        else:
2222            s_kernel = self.models[structure_factor]()
2223            p_kernel = self.kernel_module
2224            # need to reset multiplicity to get the right product
2225            if p_kernel.is_multiplicity_model:
2226                p_kernel.multiplicity = p_kernel.multiplicity_info.number
2227
2228            p_pars_len = len(p_kernel._model_info.parameters.kernel_parameters)
2229            s_pars_len = len(s_kernel._model_info.parameters.kernel_parameters)
2230
2231            self.kernel_module = MultiplicationModel(p_kernel, s_kernel)
2232            # Modify the name to correspond to shown items
2233            self.kernel_module.name = self.modelName()
2234            all_params = self.kernel_module._model_info.parameters.kernel_parameters
2235            all_param_names = [param.name for param in all_params]
2236
2237            # S(Q) params from the product model are not necessarily the same as those from the S(Q) model; any
2238            # conflicting names with P(Q) params will cause a rename
2239
2240            if "radius_effective_mode" in all_param_names:
2241                # Show all parameters
2242                # In this case, radius_effective is NOT pruned by sasmodels.product
2243                s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len])
2244                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters)
2245                product_params = modelinfo.ParameterTable(
2246                        self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
2247            else:
2248                # Ensure radius_effective is not displayed
2249                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters[1:])
2250                if "radius_effective" in all_param_names:
2251                    # In this case, radius_effective is NOT pruned by sasmodels.product
2252                    s_params = modelinfo.ParameterTable(all_params[p_pars_len+1:p_pars_len+s_pars_len])
2253                    product_params = modelinfo.ParameterTable(
2254                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
2255                else:
2256                    # In this case, radius_effective is pruned by sasmodels.product
2257                    s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len-1])
2258                    product_params = modelinfo.ParameterTable(
2259                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len-1:])
2260
2261        # Add heading row
2262        FittingUtilities.addHeadingRowToModel(self._model_model, structure_factor)
2263
2264        # Get new rows for QModel
2265        # Any renamed parameters are stored as data in the relevant item, for later handling
2266        FittingUtilities.addSimpleParametersToModel(
2267                parameters=s_params,
2268                is2D=self.is2D,
2269                parameters_original=s_params_orig,
2270                model=self._model_model,
2271                view=self.lstParams)
2272
2273        # Insert product-only params into QModel
2274        if product_params:
2275            prod_rows = FittingUtilities.addSimpleParametersToModel(
2276                    parameters=product_params,
2277                    is2D=self.is2D,
2278                    parameters_original=None,
2279                    model=self._model_model,
2280                    view=self.lstParams,
2281                    row_num=2)
2282
2283            # Since this all happens after shells are dealt with and we've inserted rows, fix this counter
2284            self._n_shells_row += len(prod_rows)
2285
2286    def haveParamsToFit(self):
2287        """
2288        Finds out if there are any parameters ready to be fitted
2289        """
2290        if not self.logic.data_is_loaded:
2291            return False
2292        if self.main_params_to_fit:
2293            return True
2294        if self.chkPolydispersity.isChecked() and self.poly_params_to_fit:
2295            return True
2296        if self.chkMagnetism.isChecked() and self.canHaveMagnetism() and self.magnet_params_to_fit:
2297            return True
2298        return False
2299
2300    def onMainParamsChange(self, top, bottom):
2301        """
2302        Callback method for updating the sasmodel parameters with the GUI values
2303        """
2304        item = self._model_model.itemFromIndex(top)
2305
2306        model_column = item.column()
2307
2308        if model_column == 0:
2309            self.checkboxSelected(item)
2310            self.cmdFit.setEnabled(self.haveParamsToFit())
2311            # Update state stack
2312            self.updateUndo()
2313            return
2314
2315        model_row = item.row()
2316        name_index = self._model_model.index(model_row, 0)
2317        name_item = self._model_model.itemFromIndex(name_index)
2318
2319        # Extract changed value.
2320        try:
2321            value = GuiUtils.toDouble(item.text())
2322        except TypeError:
2323            # Unparsable field
2324            return
2325
2326        # if the item has user data, this is the actual parameter name (e.g. to handle duplicate names)
2327        if name_item.data(QtCore.Qt.UserRole):
2328            parameter_name = str(name_item.data(QtCore.Qt.UserRole))
2329        else:
2330            parameter_name = str(self._model_model.data(name_index))
2331
2332        # Update the parameter value - note: this supports +/-inf as well
2333        param_column = self.lstParams.itemDelegate().param_value
2334        min_column = self.lstParams.itemDelegate().param_min
2335        max_column = self.lstParams.itemDelegate().param_max
2336        if model_column == param_column:
2337            # don't try to update multiplicity counters if they aren't there.
2338            # Note that this will fail for proper bad update where the model
2339            # doesn't contain multiplicity parameter
2340            if parameter_name != self.kernel_module.multiplicity_info.control:
2341                self.kernel_module.setParam(parameter_name, value)
2342        elif model_column == min_column:
2343            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
2344            self.kernel_module.details[parameter_name][1] = value
2345        elif model_column == max_column:
2346            self.kernel_module.details[parameter_name][2] = value
2347        else:
2348            # don't update the chart
2349            return
2350
2351        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
2352        # TODO: multishell params in self.kernel_module.details[??] = value
2353
2354        # handle display of effective radius parameter according to radius_effective_mode; pass ER into model if
2355        # necessary
2356        self.processEffectiveRadius()
2357
2358        # Force the chart update when actual parameters changed
2359        if model_column == 1:
2360            self.recalculatePlotData()
2361
2362        # Update state stack
2363        self.updateUndo()
2364
2365    def processEffectiveRadius(self):
2366        """
2367        Checks the value of radius_effective_mode, if existent, and processes radius_effective as necessary.
2368        * mode == 0: This means 'unconstrained'; ensure use can specify ER.
2369        * mode > 0: This means it is constrained to a P(Q)-computed value in sasmodels; prevent user from editing ER.
2370
2371        Note: If ER has been computed, it is passed back to SasView as an intermediate result. That value must be
2372        displayed for the user; that is not dealt with here, but in complete1D.
2373        """
2374        ER_row = self.getRowFromName("radius_effective")
2375        if ER_row is None:
2376            return
2377
2378        ER_mode_row = self.getRowFromName("radius_effective_mode")
2379        if ER_mode_row is None:
2380            return
2381
2382        try:
2383            ER_mode = int(self._model_model.item(ER_mode_row, 1).text())
2384        except ValueError:
2385            logging.error("radius_effective_mode was set to an invalid value.")
2386            return
2387
2388        if ER_mode == 0:
2389            # ensure the ER value can be modified by user
2390            self.setParamEditableByRow(ER_row, True)
2391        elif ER_mode > 0:
2392            # ensure the ER value cannot be modified by user
2393            self.setParamEditableByRow(ER_row, False)
2394        else:
2395            logging.error("radius_effective_mode was set to an invalid value.")
2396
2397    def setParamEditableByRow(self, row, editable=True):
2398        """
2399        Sets whether the user can edit a parameter in the table. If they cannot, the parameter name's font is changed,
2400        the value itself cannot be edited if clicked on, and the parameter may not be fitted.
2401        """
2402        item_name = self._model_model.item(row, 0)
2403        item_value = self._model_model.item(row, 1)
2404
2405        item_value.setEditable(editable)
2406
2407        if editable:
2408            # reset font
2409            item_name.setFont(QtGui.QFont())
2410            # reset colour
2411            item_name.setForeground(QtGui.QBrush())
2412            # make checkable
2413            item_name.setCheckable(True)
2414        else:
2415            # change font
2416            font = QtGui.QFont()
2417            font.setItalic(True)
2418            item_name.setFont(font)
2419            # change colour
2420            item_name.setForeground(QtGui.QBrush(QtGui.QColor(50, 50, 50)))
2421            # make not checkable (and uncheck)
2422            item_name.setCheckState(QtCore.Qt.Unchecked)
2423            item_name.setCheckable(False)
2424
2425    def isCheckable(self, row):
2426        return self._model_model.item(row, 0).isCheckable()
2427
2428    def checkboxSelected(self, item):
2429        # Assure we're dealing with checkboxes
2430        if not item.isCheckable():
2431            return
2432        status = item.checkState()
2433
2434        # If multiple rows selected - toggle all of them, filtering uncheckable
2435        # Convert to proper indices and set requested enablement
2436        self.setParameterSelection(status)
2437
2438        # update the list of parameters to fit
2439        self.main_params_to_fit = self.checkedListFromModel(self._model_model)
2440
2441    def checkedListFromModel(self, model):
2442        """
2443        Returns list of checked parameters for given model
2444        """
2445        def isChecked(row):
2446            return model.item(row, 0).checkState() == QtCore.Qt.Checked
2447
2448        return [str(model.item(row_index, 0).text())
2449                for row_index in range(model.rowCount())
2450                if isChecked(row_index)]
2451
2452    def createNewIndex(self, fitted_data):
2453        """
2454        Create a model or theory index with passed Data1D/Data2D
2455        """
2456        if self.data_is_loaded:
2457            if not fitted_data.name:
2458                name = self.nameForFittedData(self.data.filename)
2459                fitted_data.title = name
2460                fitted_data.name = name
2461                fitted_data.filename = name
2462                fitted_data.symbol = "Line"
2463            self.updateModelIndex(fitted_data)
2464        else:
2465            if not fitted_data.name:
2466                name = self.nameForFittedData(self.kernel_module.id)
2467            else:
2468                name = fitted_data.name
2469            fitted_data.title = name
2470            fitted_data.filename = name
2471            fitted_data.symbol = "Line"
2472            self.createTheoryIndex(fitted_data)
2473            # Switch to the theory tab for user's glee
2474            self.communicate.changeDataExplorerTabSignal.emit(1)
2475
2476    def updateModelIndex(self, fitted_data):
2477        """
2478        Update a QStandardModelIndex containing model data
2479        """
2480        name = self.nameFromData(fitted_data)
2481        # Make this a line if no other defined
2482        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
2483            fitted_data.symbol = 'Line'
2484        # Notify the GUI manager so it can update the main model in DataExplorer
2485        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
2486
2487    def createTheoryIndex(self, fitted_data):
2488        """
2489        Create a QStandardModelIndex containing model data
2490        """
2491        name = self.nameFromData(fitted_data)
2492        # Notify the GUI manager so it can create the theory model in DataExplorer
2493        self.theory_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
2494        self.communicate.updateTheoryFromPerspectiveSignal.emit(self.theory_item)
2495
2496    def nameFromData(self, fitted_data):
2497        """
2498        Return name for the dataset. Terribly impure function.
2499        """
2500        if fitted_data.name is None:
2501            name = self.nameForFittedData(self.logic.data.filename)
2502            fitted_data.title = name
2503            fitted_data.name = name
2504            fitted_data.filename = name
2505        else:
2506            name = fitted_data.name
2507        return name
2508
2509    def methodCalculateForData(self):
2510        '''return the method for data calculation'''
2511        return Calc1D if isinstance(self.data, Data1D) else Calc2D
2512
2513    def methodCompleteForData(self):
2514        '''return the method for result parsin on calc complete '''
2515        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
2516
2517    def updateKernelModelWithExtraParams(self, model=None):
2518        """
2519        Updates kernel model 'model' with extra parameters from
2520        the polydisp and magnetism tab, if the tabs are enabled
2521        """
2522        if model is None: return
2523        if not hasattr(model, 'setParam'): return
2524
2525        # add polydisperse parameters if asked
2526        if self.chkPolydispersity.isChecked() and self._poly_model.rowCount() > 0:
2527            for key, value in self.poly_params.items():
2528                model.setParam(key, value)
2529        # add magnetic params if asked
2530        if self.chkMagnetism.isChecked() and self.canHaveMagnetism() and self._magnet_model.rowCount() > 0:
2531            for key, value in self.magnet_params.items():
2532                model.setParam(key, value)
2533
2534    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
2535        """
2536        Wrapper for Calc1D/2D calls
2537        """
2538        if data is None:
2539            data = self.data
2540        if model is None:
2541            model = copy.deepcopy(self.kernel_module)
2542            self.updateKernelModelWithExtraParams(model)
2543
2544        if completefn is None:
2545            completefn = self.methodCompleteForData()
2546        smearer = self.smearing_widget.smearer()
2547        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2548
2549        # Disable buttons/table
2550        self.disableInteractiveElementsOnCalculate()
2551        # Awful API to a backend method.
2552        calc_thread = self.methodCalculateForData()(data=data,
2553                                               model=model,
2554                                               page_id=0,
2555                                               qmin=self.q_range_min,
2556                                               qmax=self.q_range_max,
2557                                               smearer=smearer,
2558                                               state=None,
2559                                               weight=weight,
2560                                               fid=None,
2561                                               toggle_mode_on=False,
2562                                               completefn=completefn,
2563                                               update_chisqr=True,
2564                                               exception_handler=self.calcException,
2565                                               source=None)
2566        if use_threads:
2567            if LocalConfig.USING_TWISTED:
2568                # start the thread with twisted
2569                thread = threads.deferToThread(calc_thread.compute)
2570                thread.addCallback(completefn)
2571                thread.addErrback(self.calculateDataFailed)
2572            else:
2573                # Use the old python threads + Queue
2574                calc_thread.queue()
2575                calc_thread.ready(2.5)
2576        else:
2577            results = calc_thread.compute()
2578            completefn(results)
2579
2580    def calculateQGridForModel(self):
2581        """
2582        Prepare the fitting data object, based on current ModelModel
2583        """
2584        if self.kernel_module is None:
2585            return
2586        self.calculateQGridForModelExt()
2587
2588    def calculateDataFailed(self, reason):
2589        """
2590        Thread returned error
2591        """
2592        # Bring the GUI to normal state
2593        self.enableInteractiveElements()
2594        print("Calculate Data failed with ", reason)
2595
2596    def completed1D(self, return_data):
2597        self.Calc1DFinishedSignal.emit(return_data)
2598
2599    def completed2D(self, return_data):
2600        self.Calc2DFinishedSignal.emit(return_data)
2601
2602    def complete1D(self, return_data):
2603        """
2604        Plot the current 1D data
2605        """
2606        # Bring the GUI to normal state
2607        self.enableInteractiveElements()
2608        if return_data is None:
2609            return
2610        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
2611
2612        # assure the current index is set properly for batch
2613        if len(self._logic) > 1:
2614            for i, logic in enumerate(self._logic):
2615                if logic.data.name in fitted_data.name:
2616                    self.data_index = i
2617
2618        residuals = self.calculateResiduals(fitted_data)
2619        self.model_data = fitted_data
2620        new_plots = [fitted_data]
2621        if residuals is not None:
2622            new_plots.append(residuals)
2623
2624        if self.data_is_loaded:
2625            # delete any plots associated with the data that were not updated
2626            # (e.g. to remove beta(Q), S_eff(Q))
2627            GuiUtils.deleteRedundantPlots(self.all_data[self.data_index], new_plots)
2628            pass
2629        else:
2630            # delete theory items for the model, in order to get rid of any
2631            # redundant items, e.g. beta(Q), S_eff(Q)
2632            self.communicate.deleteIntermediateTheoryPlotsSignal.emit(self.kernel_module.id)
2633
2634        # Create plots for parameters with enabled polydispersity
2635        for plot in FittingUtilities.plotPolydispersities(return_data.get('model', None)):
2636            data_id = fitted_data.id.split()
2637            plot.id = "{} [{}] {}".format(data_id[0], plot.name, " ".join(data_id[1:]))
2638            data_name = fitted_data.name.split()
2639            plot.name = " ".join([data_name[0], plot.name] + data_name[1:])
2640            self.createNewIndex(plot)
2641            new_plots.append(plot)
2642
2643        # Create plots for intermediate product data
2644        plots = self.logic.new1DProductPlots(return_data, self.tab_id)
2645        for plot in plots:
2646            plot.symbol = "Line"
2647            self.createNewIndex(plot)
2648            new_plots.append(plot)
2649
2650        for plot in new_plots:
2651            self.communicate.plotUpdateSignal.emit([plot])
2652
2653        # Update radius_effective if relevant
2654        self.updateEffectiveRadius(return_data)
2655
2656    def complete2D(self, return_data):
2657        """
2658        Plot the current 2D data
2659        """
2660        # Bring the GUI to normal state
2661        self.enableInteractiveElements()
2662
2663        if return_data is None:
2664            return
2665
2666        fitted_data = self.logic.new2DPlot(return_data)
2667        # assure the current index is set properly for batch
2668        if len(self._logic) > 1:
2669            for i, logic in enumerate(self._logic):
2670                if logic.data.name in fitted_data.name:
2671                    self.data_index = i
2672
2673        residuals = self.calculateResiduals(fitted_data)
2674        self.model_data = fitted_data
2675        new_plots = [fitted_data]
2676        if residuals is not None:
2677            new_plots.append(residuals)
2678
2679        # Update/generate plots
2680        for plot in new_plots:
2681            self.communicate.plotUpdateSignal.emit([plot])
2682
2683    def updateEffectiveRadius(self, return_data):
2684        """
2685        Given return data from sasmodels, update the effective radius parameter in the GUI table with the new
2686        calculated value as returned by sasmodels (if the value was returned).
2687        """
2688        ER_mode_row = self.getRowFromName("radius_effective_mode")
2689        if ER_mode_row is None:
2690            return
2691        try:
2692            ER_mode = int(self._model_model.item(ER_mode_row, 1).text())
2693        except ValueError:
2694            logging.error("radius_effective_mode was set to an invalid value.")
2695            return
2696        if ER_mode < 1:
2697            # does not need updating if it is not being computed
2698            return
2699
2700        ER_row = self.getRowFromName("radius_effective")
2701        if ER_row is None:
2702            return
2703
2704        scalar_results = self.logic.getScalarIntermediateResults(return_data)
2705        ER_value = scalar_results.get("effective_radius") # note name of key
2706        if ER_value is None:
2707            return
2708        # ensure the model does not recompute when updating the value
2709        self._model_model.blockSignals(True)
2710        self._model_model.item(ER_row, 1).setText(str(ER_value))
2711        self._model_model.blockSignals(False)
2712        # ensure the view is updated immediately
2713        self._model_model.layoutChanged.emit()
2714
2715    def calculateResiduals(self, fitted_data):
2716        """
2717        Calculate and print Chi2 and display chart of residuals. Returns residuals plot object.
2718        """
2719        # Create a new index for holding data
2720        fitted_data.symbol = "Line"
2721
2722        # Modify fitted_data with weighting
2723        weighted_data = self.addWeightingToData(fitted_data)
2724
2725        self.createNewIndex(weighted_data)
2726
2727        # Calculate difference between return_data and logic.data
2728        self.chi2 = FittingUtilities.calculateChi2(weighted_data, self.data)
2729        # Update the control
2730        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
2731        self.lblChi2Value.setText(chi2_repr)
2732
2733        # Plot residuals if actual data
2734        if not self.data_is_loaded:
2735            return
2736
2737        residuals_plot = FittingUtilities.plotResiduals(self.data, weighted_data)
2738        if residuals_plot is None:
2739            return
2740        residuals_plot.id = "Residual " + residuals_plot.id
2741        residuals_plot.plot_role = Data1D.ROLE_RESIDUAL
2742        self.createNewIndex(residuals_plot)
2743        return residuals_plot
2744
2745    def onCategoriesChanged(self):
2746            """
2747            Reload the category/model comboboxes
2748            """
2749            # Store the current combo indices
2750            current_cat = self.cbCategory.currentText()
2751            current_model = self.cbModel.currentText()
2752
2753            # reread the category file and repopulate the combo
2754            self.cbCategory.blockSignals(True)
2755            self.cbCategory.clear()
2756            self.readCategoryInfo()
2757            self.initializeCategoryCombo()
2758
2759            # Scroll back to the original index in Categories
2760            new_index = self.cbCategory.findText(current_cat)
2761            if new_index != -1:
2762                self.cbCategory.setCurrentIndex(new_index)
2763            self.cbCategory.blockSignals(False)
2764            # ...and in the Models
2765            self.cbModel.blockSignals(True)
2766            new_index = self.cbModel.findText(current_model)
2767            if new_index != -1:
2768                self.cbModel.setCurrentIndex(new_index)
2769            self.cbModel.blockSignals(False)
2770
2771            return
2772
2773    def calcException(self, etype, value, tb):
2774        """
2775        Thread threw an exception.
2776        """
2777        # Bring the GUI to normal state
2778        self.enableInteractiveElements()
2779        # TODO: remimplement thread cancellation
2780        logger.error("".join(traceback.format_exception(etype, value, tb)))
2781
2782    def setTableProperties(self, table):
2783        """
2784        Setting table properties
2785        """
2786        # Table properties
2787        table.verticalHeader().setVisible(False)
2788        table.setAlternatingRowColors(True)
2789        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2790        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
2791        table.resizeColumnsToContents()
2792
2793        # Header
2794        header = table.horizontalHeader()
2795        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2796        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
2797
2798        # Qt5: the following 2 lines crash - figure out why!
2799        # Resize column 0 and 7 to content
2800        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2801        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
2802
2803    def setPolyModel(self):
2804        """
2805        Set polydispersity values
2806        """
2807        if not self.model_parameters:
2808            return
2809        self._poly_model.clear()
2810
2811        parameters = self.model_parameters.form_volume_parameters
2812        if self.is2D:
2813            parameters += self.model_parameters.orientation_parameters
2814
2815        [self.setPolyModelParameters(i, param) for i, param in \
2816            enumerate(parameters) if param.polydisperse]
2817
2818        FittingUtilities.addPolyHeadersToModel(self._poly_model)
2819
2820    def setPolyModelParameters(self, i, param):
2821        """
2822        Standard of multishell poly parameter driver
2823        """
2824        param_name = param.name
2825        # see it the parameter is multishell
2826        if '[' in param.name:
2827            # Skip empty shells
2828            if self.current_shell_displayed == 0:
2829                return
2830            else:
2831                # Create as many entries as current shells
2832                for ishell in range(1, self.current_shell_displayed+1):
2833                    # Remove [n] and add the shell numeral
2834                    name = param_name[0:param_name.index('[')] + str(ishell)
2835                    self.addNameToPolyModel(i, name)
2836        else:
2837            # Just create a simple param entry
2838            self.addNameToPolyModel(i, param_name)
2839
2840    def addNameToPolyModel(self, i, param_name):
2841        """
2842        Creates a checked row in the poly model with param_name
2843        """
2844        # Polydisp. values from the sasmodel
2845        width = self.kernel_module.getParam(param_name + '.width')
2846        npts = self.kernel_module.getParam(param_name + '.npts')
2847        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2848        _, min, max = self.kernel_module.details[param_name]
2849
2850        # Update local param dict
2851        self.poly_params[param_name + '.width'] = width
2852        self.poly_params[param_name + '.npts'] = npts
2853        self.poly_params[param_name + '.nsigmas'] = nsigs
2854
2855        # Construct a row with polydisp. related variable.
2856        # This will get added to the polydisp. model
2857        # Note: last argument needs extra space padding for decent display of the control
2858        checked_list = ["Distribution of " + param_name, str(width),
2859                        str(min), str(max),
2860                        str(npts), str(nsigs), "gaussian      ",'']
2861        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2862
2863        # All possible polydisp. functions as strings in combobox
2864        func = QtWidgets.QComboBox()
2865        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
2866        # Set the default index
2867        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
2868        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2869        self.lstPoly.setIndexWidget(ind, func)
2870        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2871
2872    def onPolyFilenameChange(self, row_index):
2873        """
2874        Respond to filename_updated signal from the delegate
2875        """
2876        # For the given row, invoke the "array" combo handler
2877        array_caption = 'array'
2878
2879        # Get the combo box reference
2880        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2881        widget = self.lstPoly.indexWidget(ind)
2882
2883        # Update the combo box so it displays "array"
2884        widget.blockSignals(True)
2885        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
2886        widget.blockSignals(False)
2887
2888        # Invoke the file reader
2889        self.onPolyComboIndexChange(array_caption, row_index)
2890
2891    def onPolyComboIndexChange(self, combo_string, row_index):
2892        """
2893        Modify polydisp. defaults on function choice
2894        """
2895        # Get npts/nsigs for current selection
2896        param = self.model_parameters.form_volume_parameters[row_index]
2897        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2898        combo_box = self.lstPoly.indexWidget(file_index)
2899
2900        def updateFunctionCaption(row):
2901            # Utility function for update of polydispersity function name in the main model
2902            if not self.isCheckable(row):
2903                return
2904            self._model_model.blockSignals(True)
2905            param_name = str(self._model_model.item(row, 0).text())
2906            self._model_model.blockSignals(False)
2907            if param_name !=  param.name:
2908                return
2909            # Modify the param value
2910            self._model_model.blockSignals(True)
2911            if self.has_error_column:
2912                # err column changes the indexing
2913                self._model_model.item(row, 0).child(0).child(0,5).setText(combo_string)
2914            else:
2915                self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
2916            self._model_model.blockSignals(False)
2917
2918        if combo_string == 'array':
2919            try:
2920                self.loadPolydispArray(row_index)
2921                # Update main model for display
2922                self.iterateOverModel(updateFunctionCaption)
2923                # disable the row
2924                lo = self.lstPoly.itemDelegate().poly_pd
2925                hi = self.lstPoly.itemDelegate().poly_function
2926                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
2927                return
2928            except IOError:
2929                combo_box.setCurrentIndex(self.orig_poly_index)
2930                # Pass for cancel/bad read
2931                pass
2932
2933        # Enable the row in case it was disabled by Array
2934        self._poly_model.blockSignals(True)
2935        max_range = self.lstPoly.itemDelegate().poly_filename
2936        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
2937        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2938        self._poly_model.setData(file_index, "")
2939        self._poly_model.blockSignals(False)
2940
2941        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
2942        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
2943
2944        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
2945        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
2946
2947        self._poly_model.setData(npts_index, npts)
2948        self._poly_model.setData(nsigs_index, nsigs)
2949
2950        self.iterateOverModel(updateFunctionCaption)
2951        self.orig_poly_index = combo_box.currentIndex()
2952
2953    def loadPolydispArray(self, row_index):
2954        """
2955        Show the load file dialog and loads requested data into state
2956        """
2957        datafile = QtWidgets.QFileDialog.getOpenFileName(
2958            self, "Choose a weight file", "", "All files (*.*)", None,
2959            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
2960
2961        if not datafile:
2962            logger.info("No weight data chosen.")
2963            raise IOError
2964
2965        values = []
2966        weights = []
2967        def appendData(data_tuple):
2968            """
2969            Fish out floats from a tuple of strings
2970            """
2971            try:
2972                values.append(float(data_tuple[0]))
2973                weights.append(float(data_tuple[1]))
2974            except (ValueError, IndexError):
2975                # just pass through if line with bad data
2976                return
2977
2978        with open(datafile, 'r') as column_file:
2979            column_data = [line.rstrip().split() for line in column_file.readlines()]
2980            [appendData(line) for line in column_data]
2981
2982        # If everything went well - update the sasmodel values
2983        self.disp_model = POLYDISPERSITY_MODELS['array']()
2984        self.disp_model.set_weights(np.array(values), np.array(weights))
2985        # + update the cell with filename
2986        fname = os.path.basename(str(datafile))
2987        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2988        self._poly_model.setData(fname_index, fname)
2989
2990    def onColumnWidthUpdate(self, index, old_size, new_size):
2991        """
2992        Simple state update of the current column widths in the  param list
2993        """
2994        self.lstParamHeaderSizes[index] = new_size
2995
2996    def setMagneticModel(self):
2997        """
2998        Set magnetism values on model
2999        """
3000        if not self.model_parameters:
3001            return
3002        self._magnet_model.clear()
3003        # default initial value
3004        m0 = 0.5
3005        for param in self.model_parameters.call_parameters:
3006            if param.type != 'magnetic': continue
3007            if "M0" in param.name:
3008                m0 += 0.5
3009                value = m0
3010            else:
3011                value = param.default
3012            self.addCheckedMagneticListToModel(param, value)
3013
3014        FittingUtilities.addHeadersToModel(self._magnet_model)
3015
3016    def shellNamesList(self):
3017        """
3018        Returns list of names of all multi-shell parameters
3019        E.g. for sld[n], radius[n], n=1..3 it will return
3020        [sld1, sld2, sld3, radius1, radius2, radius3]
3021        """
3022        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
3023        top_index = self.kernel_module.multiplicity_info.number
3024        shell_names = []
3025        for i in range(1, top_index+1):
3026            for name in multi_names:
3027                shell_names.append(name+str(i))
3028        return shell_names
3029
3030    def addCheckedMagneticListToModel(self, param, value):
3031        """
3032        Wrapper for model update with a subset of magnetic parameters
3033        """
3034        try:
3035            basename, _ = param.name.rsplit('_', 1)
3036        except ValueError:
3037            basename = param.name
3038        if basename in self.shell_names:
3039            try:
3040                shell_index = int(basename[-2:])
3041            except ValueError:
3042                shell_index = int(basename[-1:])
3043
3044            if shell_index > self.current_shell_displayed:
3045                return
3046
3047        checked_list = [param.name,
3048                        str(value),
3049                        str(param.limits[0]),
3050                        str(param.limits[1]),
3051                        param.units]
3052
3053        self.magnet_params[param.name] = value
3054
3055        FittingUtilities.addCheckedListToModel(self._magnet_model, checked_list)
3056
3057    def enableStructureFactorControl(self, structure_factor):
3058        """
3059        Add structure factors to the list of parameters
3060        """
3061        if self.kernel_module.is_form_factor or structure_factor == 'None':
3062            self.enableStructureCombo()
3063        else:
3064            self.disableStructureCombo()
3065
3066    def addExtraShells(self):
3067        """
3068        Add a combobox for multiple shell display
3069        """
3070        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
3071
3072        if param_length == 0:
3073            return
3074
3075        # cell 1: variable name
3076        item1 = QtGui.QStandardItem(param_name)
3077
3078        func = QtWidgets.QComboBox()
3079
3080        # cell 2: combobox
3081        item2 = QtGui.QStandardItem()
3082
3083        # cell 3: min value
3084        item3 = QtGui.QStandardItem()
3085
3086        # cell 4: max value
3087        item4 = QtGui.QStandardItem()
3088
3089        # cell 4: SLD button
3090        item5 = QtGui.QStandardItem()
3091        button = QtWidgets.QPushButton()
3092        button.setText("Show SLD Profile")
3093
3094        self._model_model.appendRow([item1, item2, item3, item4, item5])
3095
3096        # Beautify the row:  span columns 2-4
3097        shell_row = self._model_model.rowCount()
3098        shell_index = self._model_model.index(shell_row-1, 1)
3099        button_index = self._model_model.index(shell_row-1, 4)
3100
3101        self.lstParams.setIndexWidget(shell_index, func)
3102        self.lstParams.setIndexWidget(button_index, button)
3103        self._n_shells_row = shell_row - 1
3104
3105        # Get the default number of shells for the model
3106        kernel_pars = self.kernel_module._model_info.parameters.kernel_parameters
3107        shell_par = None
3108        for par in kernel_pars:
3109            parname = par.name
3110            if '[' in parname:
3111                 parname = parname[:parname.index('[')]
3112            if parname == param_name:
3113                shell_par = par
3114                break
3115        if shell_par is None:
3116            logger.error("Could not find %s in kernel parameters.", param_name)
3117            return
3118        default_shell_count = shell_par.default
3119        shell_min = 0
3120        shell_max = 0
3121        try:
3122            shell_min = int(shell_par.limits[0])
3123            shell_max = int(shell_par.limits[1])
3124        except IndexError as ex:
3125            # no info about limits
3126            pass
3127        except Exception as ex:
3128            logging.error("Badly defined multiplicity: "+ str(ex))
3129            return
3130        # don't update the kernel here - this data is display only
3131        self._model_model.blockSignals(True)
3132        item3.setText(str(shell_min))
3133        item4.setText(str(shell_max))
3134        self._model_model.blockSignals(False)
3135
3136        ## Respond to index change
3137        #func.currentTextChanged.connect(self.modifyShellsInList)
3138
3139        # Respond to button press
3140        button.clicked.connect(self.onShowSLDProfile)
3141
3142        # Available range of shells displayed in the combobox
3143        func.addItems([str(i) for i in range(shell_min, shell_max+1)])
3144
3145        # Respond to index change
3146        func.currentTextChanged.connect(self.modifyShellsInList)
3147
3148        # Add default number of shells to the model
3149        func.setCurrentText(str(default_shell_count))
3150        self.modifyShellsInList(str(default_shell_count))
3151
3152    def modifyShellsInList(self, text):
3153        """
3154        Add/remove additional multishell parameters
3155        """
3156        # Find row location of the combobox
3157        first_row = self._n_shells_row + 1
3158        remove_rows = self._num_shell_params
3159        try:
3160            index = int(text)
3161        except ValueError:
3162            # bad text on the control!
3163            index = 0
3164            logger.error("Multiplicity incorrect! Setting to 0")
3165        self.kernel_module.multiplicity = index
3166        if remove_rows > 1:
3167            self._model_model.removeRows(first_row, remove_rows)
3168
3169        new_rows = FittingUtilities.addShellsToModel(
3170                self.model_parameters,
3171                self._model_model,
3172                index,
3173                first_row,
3174                self.lstParams)
3175
3176        self._num_shell_params = len(new_rows)
3177        self.current_shell_displayed = index
3178
3179        # Param values for existing shells were reset to default; force all changes into kernel module
3180        for row in new_rows:
3181            par = row[0].text()
3182            val = GuiUtils.toDouble(row[1].text())
3183            self.kernel_module.setParam(par, val)
3184
3185        # Change 'n' in the parameter model; also causes recalculation
3186        self._model_model.item(self._n_shells_row, 1).setText(str(index))
3187
3188        # Update relevant models
3189        self.setPolyModel()
3190        if self.canHaveMagnetism():
3191            self.setMagneticModel()
3192
3193    def onShowSLDProfile(self):
3194        """
3195        Show a quick plot of SLD profile
3196        """
3197        # get profile data
3198        try:
3199            x, y = self.kernel_module.getProfile()
3200        except TypeError:
3201            msg = "SLD profile calculation failed."
3202            logging.error(msg)
3203            return
3204
3205        y *= 1.0e6
3206        profile_data = Data1D(x=x, y=y)
3207        profile_data.name = "SLD"
3208        profile_data.scale = 'linear'
3209        profile_data.symbol = 'Line'
3210        profile_data.hide_error = True
3211        profile_data._xaxis = "R(\AA)"
3212        profile_data._yaxis = "SLD(10^{-6}\AA^{-2})"
3213
3214        plotter = PlotterWidget(self, quickplot=True)
3215        plotter.data = profile_data
3216        plotter.showLegend = True
3217        plotter.plot(hide_error=True, marker='-')
3218
3219        self.plot_widget = QtWidgets.QWidget()
3220        self.plot_widget.setWindowTitle("Scattering Length Density Profile")
3221        layout = QtWidgets.QVBoxLayout()
3222        layout.addWidget(plotter)
3223        self.plot_widget.setLayout(layout)
3224        self.plot_widget.show()
3225
3226    def setInteractiveElements(self, enabled=True):
3227        """
3228        Switch interactive GUI elements on/off
3229        """
3230        assert isinstance(enabled, bool)
3231
3232        self.lstParams.setEnabled(enabled)
3233        self.lstPoly.setEnabled(enabled)
3234        self.lstMagnetic.setEnabled(enabled)
3235
3236        self.cbCategory.setEnabled(enabled)
3237
3238        if enabled:
3239            # worry about original enablement of model and SF
3240            self.cbModel.setEnabled(self.enabled_cbmodel)
3241            self.cbStructureFactor.setEnabled(self.enabled_sfmodel)
3242        else:
3243            self.cbModel.setEnabled(enabled)
3244            self.cbStructureFactor.setEnabled(enabled)
3245
3246        self.cmdPlot.setEnabled(enabled)
3247
3248    def enableInteractiveElements(self):
3249        """
3250        Set buttion caption on fitting/calculate finish
3251        Enable the param table(s)
3252        """
3253        # Notify the user that fitting is available
3254        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
3255        self.cmdFit.setText("Fit")
3256        self.fit_started = False
3257        self.setInteractiveElements(True)
3258
3259    def disableInteractiveElements(self):
3260        """
3261        Set buttion caption on fitting/calculate start
3262        Disable the param table(s)
3263        """
3264        # Notify the user that fitting is being run
3265        # Allow for stopping the job
3266        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
3267        self.cmdFit.setText('Stop fit')
3268        self.setInteractiveElements(False)
3269
3270    def disableInteractiveElementsOnCalculate(self):
3271        """
3272        Set buttion caption on fitting/calculate start
3273        Disable the param table(s)
3274        """
3275        # Notify the user that fitting is being run
3276        # Allow for stopping the job
3277        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
3278        self.cmdFit.setText('Running...')
3279        self.setInteractiveElements(False)
3280
3281    def readFitPage(self, fp):
3282        """
3283        Read in state from a fitpage object and update GUI
3284        """
3285        assert isinstance(fp, FitPage)
3286        # Main tab info
3287        self.logic.data.filename = fp.filename
3288        self.data_is_loaded = fp.data_is_loaded
3289        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
3290        self.chkMagnetism.setCheckState(fp.is_magnetic)
3291        self.chk2DView.setCheckState(fp.is2D)
3292
3293        # Update the comboboxes
3294        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
3295        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
3296        if fp.current_factor:
3297            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
3298
3299        self.chi2 = fp.chi2
3300
3301        # Options tab
3302        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
3303        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
3304        self.npts = fp.fit_options[fp.NPTS]
3305        self.log_points = fp.fit_options[fp.LOG_POINTS]
3306        self.weighting = fp.fit_options[fp.WEIGHTING]
3307
3308        # Models
3309        self._model_model = fp.model_model
3310        self._poly_model = fp.poly_model
3311        self._magnet_model = fp.magnetism_model
3312
3313        # Resolution tab
3314        smearing = fp.smearing_options[fp.SMEARING_OPTION]
3315        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
3316        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
3317        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
3318        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
3319
3320        # TODO: add polidyspersity and magnetism
3321
3322    def saveToFitPage(self, fp):
3323        """
3324        Write current state to the given fitpage
3325        """
3326        assert isinstance(fp, FitPage)
3327
3328        # Main tab info
3329        fp.filename = self.logic.data.filename
3330        fp.data_is_loaded = self.data_is_loaded
3331        fp.is_polydisperse = self.chkPolydispersity.isChecked()
3332        fp.is_magnetic = self.chkMagnetism.isChecked()
3333        fp.is2D = self.chk2DView.isChecked()
3334        fp.data = self.data
3335
3336        # Use current models - they contain all the required parameters
3337        fp.model_model = self._model_model
3338        fp.poly_model = self._poly_model
3339        fp.magnetism_model = self._magnet_model
3340
3341        if self.cbCategory.currentIndex() != 0:
3342            fp.current_category = str(self.cbCategory.currentText())
3343            fp.current_model = str(self.cbModel.currentText())
3344
3345        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
3346            fp.current_factor = str(self.cbStructureFactor.currentText())
3347        else:
3348            fp.current_factor = ''
3349
3350        fp.chi2 = self.chi2
3351        fp.main_params_to_fit = self.main_params_to_fit
3352        fp.poly_params_to_fit = self.poly_params_to_fit
3353        fp.magnet_params_to_fit = self.magnet_params_to_fit
3354        fp.kernel_module = self.kernel_module
3355
3356        # Algorithm options
3357        # fp.algorithm = self.parent.fit_options.selected_id
3358
3359        # Options tab
3360        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
3361        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
3362        fp.fit_options[fp.NPTS] = self.npts
3363        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
3364        fp.fit_options[fp.LOG_POINTS] = self.log_points
3365        fp.fit_options[fp.WEIGHTING] = self.weighting
3366
3367        # Resolution tab
3368        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3369        fp.smearing_options[fp.SMEARING_OPTION] = smearing
3370        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
3371        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
3372        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
3373
3374        # TODO: add polidyspersity and magnetism
3375
3376    def updateUndo(self):
3377        """
3378        Create a new state page and add it to the stack
3379        """
3380        if self.undo_supported:
3381            self.pushFitPage(self.currentState())
3382
3383    def currentState(self):
3384        """
3385        Return fit page with current state
3386        """
3387        new_page = FitPage()
3388        self.saveToFitPage(new_page)
3389
3390        return new_page
3391
3392    def pushFitPage(self, new_page):
3393        """
3394        Add a new fit page object with current state
3395        """
3396        self.page_stack.append(new_page)
3397
3398    def popFitPage(self):
3399        """
3400        Remove top fit page from stack
3401        """
3402        if self.page_stack:
3403            self.page_stack.pop()
3404
3405    def getReport(self):
3406        """
3407        Create and return HTML report with parameters and charts
3408        """
3409        index = None
3410        if self.all_data:
3411            index = self.all_data[self.data_index]
3412        else:
3413            index = self.theory_item
3414        params = FittingUtilities.getStandardParam(self._model_model)
3415        report_logic = ReportPageLogic(self,
3416                                       kernel_module=self.kernel_module,
3417                                       data=self.data,
3418                                       index=index,
3419                                       params=params)
3420
3421        return report_logic.reportList()
3422
3423    def savePageState(self):
3424        """
3425        Create and serialize local PageState
3426        """
3427        filepath = self.saveAsAnalysisFile()
3428        if filepath is None or filepath == "":
3429            return
3430
3431        fitpage_state = self.getFitPage()
3432        fitpage_state += self.getFitModel()
3433
3434        with open(filepath, 'w') as statefile:
3435            for line in fitpage_state:
3436                statefile.write(str(line))
3437
3438        self.communicate.statusBarUpdateSignal.emit('Analysis saved.')
3439
3440    def saveAsAnalysisFile(self):
3441        """
3442        Show the save as... dialog and return the chosen filepath
3443        """
3444        default_name = "FitPage"+str(self.tab_id)+".fitv"
3445
3446        wildcard = "fitv files (*.fitv)"
3447        kwargs = {
3448            'caption'   : 'Save As',
3449            'directory' : default_name,
3450            'filter'    : wildcard,
3451            'parent'    : None,
3452        }
3453        # Query user for filename.
3454        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
3455        filename = filename_tuple[0]
3456        return filename
3457
3458    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
3459        """
3460        This is a callback method called from the CANSAS reader.
3461        We need the instance of this reader only for writing out a file,
3462        so there's nothing here.
3463        Until Load Analysis is implemented, that is.
3464        """
3465        pass
3466
3467    def loadPageState(self, pagestate=None):
3468        """
3469        Load the PageState object and update the current widget
3470        """
3471        filepath = self.loadAnalysisFile()
3472        if filepath is None or filepath == "":
3473            return
3474
3475        with open(filepath, 'r') as statefile:
3476            #column_data = [line.rstrip().split() for line in statefile.readlines()]
3477            lines = statefile.readlines()
3478
3479        # convert into list of lists
3480        pass
3481
3482    def loadAnalysisFile(self):
3483        """
3484        Called when the "Open Project" menu item chosen.
3485        """
3486        default_name = "FitPage"+str(self.tab_id)+".fitv"
3487        wildcard = "fitv files (*.fitv)"
3488        kwargs = {
3489            'caption'   : 'Open Analysis',
3490            'directory' : default_name,
3491            'filter'    : wildcard,
3492            'parent'    : self,
3493        }
3494        filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
3495        return filename
3496
3497    def onCopyToClipboard(self, format=None):
3498        """
3499        Copy current fitting parameters into the clipboard
3500        using requested formatting:
3501        plain, excel, latex
3502        """
3503        param_list = self.getFitParameters()
3504        if format=="":
3505            param_list = self.getFitPage()
3506            param_list += self.getFitModel()
3507            formatted_output = FittingUtilities.formatParameters(param_list)
3508        elif format == "Excel":
3509            formatted_output = FittingUtilities.formatParametersExcel(param_list[1:])
3510        elif format == "Latex":
3511            formatted_output = FittingUtilities.formatParametersLatex(param_list[1:])
3512        else:
3513            raise AttributeError("Bad parameter output format specifier.")
3514
3515        # Dump formatted_output to the clipboard
3516        cb = QtWidgets.QApplication.clipboard()
3517        cb.setText(formatted_output)
3518
3519    def getFitModel(self):
3520        """
3521        serializes combobox state
3522        """
3523        param_list = []
3524        model = str(self.cbModel.currentText())
3525        category = str(self.cbCategory.currentText())
3526        structure = str(self.cbStructureFactor.currentText())
3527        param_list.append(['fitpage_category', category])
3528        param_list.append(['fitpage_model', model])
3529        param_list.append(['fitpage_structure', structure])
3530
3531        return param_list
3532
3533    def getFitPage(self):
3534        """
3535        serializes full state of this fit page
3536        """
3537        # run a loop over all parameters and pull out
3538        # first - regular params
3539        param_list = self.getFitParameters()
3540
3541        param_list.append(['is_data', str(self.data_is_loaded)])
3542        if self.data_is_loaded:
3543            param_list.append(['data_id', str(self.logic.data.id)])
3544            param_list.append(['data_name', str(self.logic.data.filename)])
3545
3546        # option tab
3547        param_list.append(['q_range_min', str(self.q_range_min)])
3548        param_list.append(['q_range_max', str(self.q_range_max)])
3549        param_list.append(['q_weighting', str(self.weighting)])
3550        param_list.append(['weighting', str(self.options_widget.weighting)])
3551
3552        # resolution
3553        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3554        index = self.smearing_widget.cbSmearing.currentIndex()
3555        param_list.append(['smearing', str(index)])
3556        param_list.append(['smearing_min', str(smearing_min)])
3557        param_list.append(['smearing_max', str(smearing_max)])
3558
3559        # checkboxes, if required
3560        has_polydisp = self.chkPolydispersity.isChecked()
3561        has_magnetism = self.chkMagnetism.isChecked()
3562        has_chain = self.chkChainFit.isChecked()
3563        has_2D = self.chk2DView.isChecked()
3564        param_list.append(['polydisperse_params', str(has_polydisp)])
3565        param_list.append(['magnetic_params', str(has_magnetism)])
3566        param_list.append(['chainfit_params', str(has_chain)])
3567        param_list.append(['2D_params', str(has_2D)])
3568
3569        return param_list
3570
3571    def getFitParameters(self):
3572        """
3573        serializes current parameters
3574        """
3575        param_list = []
3576        param_list.append(['model_name', str(self.cbModel.currentText())])
3577
3578        def gatherParams(row):
3579            """
3580            Create list of main parameters based on _model_model
3581            """
3582            param_name = str(self._model_model.item(row, 0).text())
3583
3584            # Assure this is a parameter - must contain a checkbox
3585            if not self._model_model.item(row, 0).isCheckable():
3586                # maybe it is a combobox item (multiplicity)
3587                try:
3588                    index = self._model_model.index(row, 1)
3589                    widget = self.lstParams.indexWidget(index)
3590                    if widget is None:
3591                        return
3592                    if isinstance(widget, QtWidgets.QComboBox):
3593                        # find the index of the combobox
3594                        current_index = widget.currentIndex()
3595                        param_list.append([param_name, 'None', str(current_index)])
3596                except Exception as ex:
3597                    pass
3598                return
3599
3600            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3601            # Value of the parameter. In some cases this is the text of the combobox choice.
3602            param_value = str(self._model_model.item(row, 1).text())
3603            param_error = None
3604            param_min = None
3605            param_max = None
3606            column_offset = 0
3607            if self.has_error_column:
3608                column_offset = 1
3609                param_error = str(self._model_model.item(row, 1+column_offset).text())
3610            try:
3611                param_min = str(self._model_model.item(row, 2+column_offset).text())
3612                param_max = str(self._model_model.item(row, 3+column_offset).text())
3613            except:
3614                pass
3615
3616            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3617
3618        def gatherPolyParams(row):
3619            """
3620            Create list of polydisperse parameters based on _poly_model
3621            """
3622            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
3623            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3624            param_value = str(self._poly_model.item(row, 1).text())
3625            param_error = None
3626            column_offset = 0
3627            if self.has_poly_error_column:
3628                column_offset = 1
3629                param_error = str(self._poly_model.item(row, 1+column_offset).text())
3630            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
3631            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
3632            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
3633            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
3634            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
3635            index = self._poly_model.index(row, 6+column_offset)
3636            widget = self.lstPoly.indexWidget(index)
3637            if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3638                param_fun = widget.currentText()
3639            # width
3640            name = param_name+".width"
3641            param_list.append([name, param_checked, param_value, param_error,
3642                               param_min, param_max, param_npts, param_nsigs, param_fun])
3643
3644        def gatherMagnetParams(row):
3645            """
3646            Create list of magnetic parameters based on _magnet_model
3647            """
3648            param_name = str(self._magnet_model.item(row, 0).text())
3649            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3650            param_value = str(self._magnet_model.item(row, 1).text())
3651            param_error = None
3652            column_offset = 0
3653            if self.has_magnet_error_column:
3654                column_offset = 1
3655                param_error = str(self._magnet_model.item(row, 1+column_offset).text())
3656            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
3657            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
3658            param_list.append([param_name, param_checked, param_value,
3659                               param_error, param_min, param_max])
3660
3661        self.iterateOverModel(gatherParams)
3662        if self.chkPolydispersity.isChecked():
3663            self.iterateOverPolyModel(gatherPolyParams)
3664        if self.chkMagnetism.isChecked() and self.canHaveMagnetism():
3665            self.iterateOverMagnetModel(gatherMagnetParams)
3666
3667        if self.kernel_module.is_multiplicity_model:
3668            param_list.append(['multiplicity', str(self.kernel_module.multiplicity)])
3669
3670        return param_list
3671
3672    def onParameterPaste(self):
3673        """
3674        Use the clipboard to update fit state
3675        """
3676        # Check if the clipboard contains right stuff
3677        cb = QtWidgets.QApplication.clipboard()
3678        cb_text = cb.text()
3679
3680        context = {}
3681        lines = cb_text.split(':')
3682        if lines[0] != 'sasview_parameter_values':
3683            return False
3684
3685        # put the text into dictionary
3686        line_dict = {}
3687        for line in lines[1:]:
3688            content = line.split(',')
3689            if len(content) > 1:
3690                line_dict[content[0]] = content[1:]
3691
3692        model = line_dict['model_name'][0]
3693
3694        if 'model_name' not in line_dict.keys():
3695            return False
3696
3697        if 'multiplicity' in line_dict.keys():
3698            multip = int(line_dict['multiplicity'][0], 0)
3699            # reset the model with multiplicity, so further updates are saved
3700            if self.kernel_module.is_multiplicity_model:
3701                self.kernel_module.multiplicity=multip
3702                self.updateMultiplicityCombo(multip)
3703
3704        if 'polydisperse_params' in line_dict.keys():
3705            self.chkPolydispersity.setChecked(line_dict['polydisperse_params'][0]=='True')
3706        if 'magnetic_params' in line_dict.keys():
3707            self.chkMagnetism.setChecked(line_dict['magnetic_params'][0]=='True')
3708        if 'chainfit_params' in line_dict.keys():
3709            self.chkChainFit.setChecked(line_dict['chainfit_params'][0]=='True')
3710        if '2D_params' in line_dict.keys():
3711            self.chk2DView.setChecked(line_dict['2D_params'][0]=='True')
3712
3713        # Create the context dictionary for parameters
3714        context['model_name'] = model
3715        for key, value in line_dict.items():
3716            if len(value) > 2:
3717                context[key] = value
3718
3719        if str(self.cbModel.currentText()) != str(context['model_name']):
3720            msg = QtWidgets.QMessageBox()
3721            msg.setIcon(QtWidgets.QMessageBox.Information)
3722            msg.setText("The model in the clipboard is not the same as the currently loaded model. \
3723                         Not all parameters saved may paste correctly.")
3724            msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
3725            result = msg.exec_()
3726            if result == QtWidgets.QMessageBox.Ok:
3727                pass
3728            else:
3729                return
3730
3731        if 'smearing' in line_dict.keys():
3732            try:
3733                index = int(line_dict['smearing'][0])
3734                self.smearing_widget.cbSmearing.setCurrentIndex(index)
3735            except ValueError:
3736                pass
3737        if 'smearing_min' in line_dict.keys():
3738            try:
3739                self.smearing_widget.dq_l = float(line_dict['smearing_min'][0])
3740            except ValueError:
3741                pass
3742        if 'smearing_max' in line_dict.keys():
3743            try:
3744                self.smearing_widget.dq_r = float(line_dict['smearing_max'][0])
3745            except ValueError:
3746                pass
3747
3748        if 'q_range_max' in line_dict.keys():
3749            try:
3750                self.q_range_min = float(line_dict['q_range_min'][0])
3751                self.q_range_max = float(line_dict['q_range_max'][0])
3752            except ValueError:
3753                pass
3754        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
3755        try:
3756            button_id = int(line_dict['weighting'][0])
3757            for button in self.options_widget.weightingGroup.buttons():
3758                if abs(self.options_widget.weightingGroup.id(button)) == button_id+2:
3759                    button.setChecked(True)
3760                    break
3761        except ValueError:
3762            pass
3763
3764        self.updateFullModel(context)
3765        self.updateFullPolyModel(context)
3766        self.updateFullMagnetModel(context)
3767
3768    def updateMultiplicityCombo(self, multip):
3769        """
3770        Find and update the multiplicity combobox
3771        """
3772        index = self._model_model.index(self._n_shells_row, 1)
3773        widget = self.lstParams.indexWidget(index)
3774        if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3775            widget.setCurrentIndex(widget.findText(str(multip)))
3776        self.current_shell_displayed = multip
3777
3778    def updateFullModel(self, param_dict):
3779        """
3780        Update the model with new parameters
3781        """
3782        assert isinstance(param_dict, dict)
3783        if not dict:
3784            return
3785
3786        def updateFittedValues(row):
3787            # Utility function for main model update
3788            # internal so can use closure for param_dict
3789            param_name = str(self._model_model.item(row, 0).text())
3790            if param_name not in list(param_dict.keys()):
3791                return
3792            # Special case of combo box in the cell (multiplicity)
3793            param_line = param_dict[param_name]
3794            if len(param_line) == 1:
3795                # modify the shells value
3796                try:
3797                    combo_index = int(param_line[0])
3798                except ValueError:
3799                    # quietly pass
3800                    return
3801                index = self._model_model.index(row, 1)
3802                widget = self.lstParams.indexWidget(index)
3803                if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3804                    #widget.setCurrentIndex(combo_index)
3805                    return
3806            # checkbox state
3807            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3808            self._model_model.item(row, 0).setCheckState(param_checked)
3809
3810            # parameter value can be either just a value or text on the combobox
3811            param_text = param_dict[param_name][1]
3812            index = self._model_model.index(row, 1)
3813            widget = self.lstParams.indexWidget(index)
3814            if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3815                # Find the right index based on text
3816                combo_index = int(param_text, 0)
3817                widget.setCurrentIndex(combo_index)
3818            else:
3819                # modify the param value
3820                param_repr = GuiUtils.formatNumber(param_text, high=True)
3821                self._model_model.item(row, 1).setText(param_repr)
3822
3823            # Potentially the error column
3824            ioffset = 0
3825            joffset = 0
3826            if len(param_dict[param_name])>4:
3827                # error values are not editable - no need to update
3828                ioffset = 1
3829            if self.has_error_column:
3830                joffset = 1
3831            # min/max
3832            try:
3833                param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3834                self._model_model.item(row, 2+joffset).setText(param_repr)
3835                param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3836                self._model_model.item(row, 3+joffset).setText(param_repr)
3837            except:
3838                pass
3839
3840            self.setFocus()
3841
3842        self.iterateOverModel(updateFittedValues)
3843
3844    def updateFullPolyModel(self, param_dict):
3845        """
3846        Update the polydispersity model with new parameters, create the errors column
3847        """
3848        assert isinstance(param_dict, dict)
3849        if not dict:
3850            return
3851
3852        def updateFittedValues(row):
3853            # Utility function for main model update
3854            # internal so can use closure for param_dict
3855            if row >= self._poly_model.rowCount():
3856                return
3857            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
3858            if param_name not in list(param_dict.keys()):
3859                return
3860            # checkbox state
3861            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3862            self._poly_model.item(row,0).setCheckState(param_checked)
3863
3864            # modify the param value
3865            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3866            self._poly_model.item(row, 1).setText(param_repr)
3867
3868            # Potentially the error column
3869            ioffset = 0
3870            joffset = 0
3871            if len(param_dict[param_name])>7:
3872                ioffset = 1
3873            if self.has_poly_error_column:
3874                joffset = 1
3875            # min
3876            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3877            self._poly_model.item(row, 2+joffset).setText(param_repr)
3878            # max
3879            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3880            self._poly_model.item(row, 3+joffset).setText(param_repr)
3881            # Npts
3882            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
3883            self._poly_model.item(row, 4+joffset).setText(param_repr)
3884            # Nsigs
3885            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3886            self._poly_model.item(row, 5+joffset).setText(param_repr)
3887
3888            self.setFocus()
3889
3890        self.iterateOverPolyModel(updateFittedValues)
3891
3892    def updateFullMagnetModel(self, param_dict):
3893        """
3894        Update the magnetism model with new parameters, create the errors column
3895        """
3896        assert isinstance(param_dict, dict)
3897        if not dict:
3898            return
3899
3900        def updateFittedValues(row):
3901            # Utility function for main model update
3902            # internal so can use closure for param_dict
3903            if row >= self._magnet_model.rowCount():
3904                return
3905            param_name = str(self._magnet_model.item(row, 0).text()).rsplit()[-1]
3906            if param_name not in list(param_dict.keys()):
3907                return
3908            # checkbox state
3909            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3910            self._magnet_model.item(row,0).setCheckState(param_checked)
3911
3912            # modify the param value
3913            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3914            self._magnet_model.item(row, 1).setText(param_repr)
3915
3916            # Potentially the error column
3917            ioffset = 0
3918            joffset = 0
3919            if len(param_dict[param_name])>4:
3920                ioffset = 1
3921            if self.has_magnet_error_column:
3922                joffset = 1
3923            # min
3924            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3925            self._magnet_model.item(row, 2+joffset).setText(param_repr)
3926            # max
3927            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3928            self._magnet_model.item(row, 3+joffset).setText(param_repr)
3929
3930        self.iterateOverMagnetModel(updateFittedValues)
3931
3932    def getCurrentFitState(self, state=None):
3933        """
3934        Store current state for fit_page
3935        """
3936        # save model option
3937        #if self.model is not None:
3938        #    self.disp_list = self.getDispParamList()
3939        #    state.disp_list = copy.deepcopy(self.disp_list)
3940        #    #state.model = self.model.clone()
3941
3942        # Comboboxes
3943        state.categorycombobox = self.cbCategory.currentText()
3944        state.formfactorcombobox = self.cbModel.currentText()
3945        if self.cbStructureFactor.isEnabled():
3946            state.structurecombobox = self.cbStructureFactor.currentText()
3947        state.tcChi = self.chi2
3948
3949        state.enable2D = self.is2D
3950
3951        #state.weights = copy.deepcopy(self.weights)
3952        # save data
3953        state.data = copy.deepcopy(self.data)
3954
3955        # save plotting range
3956        state.qmin = self.q_range_min
3957        state.qmax = self.q_range_max
3958        state.npts = self.npts
3959
3960        #    self.state.enable_disp = self.enable_disp.GetValue()
3961        #    self.state.disable_disp = self.disable_disp.GetValue()
3962
3963        #    self.state.enable_smearer = \
3964        #                        copy.deepcopy(self.enable_smearer.GetValue())
3965        #    self.state.disable_smearer = \
3966        #                        copy.deepcopy(self.disable_smearer.GetValue())
3967
3968        #self.state.pinhole_smearer = \
3969        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
3970        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
3971        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
3972        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
3973        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
3974        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
3975
3976        p = self.model_parameters
3977        # save checkbutton state and txtcrtl values
3978        state.parameters = FittingUtilities.getStandardParam(self._model_model)
3979        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
3980
3981        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
3982        #self._copy_parameters_state(self.parameters, self.state.parameters)
3983        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
3984        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
3985
Note: See TracBrowser for help on using the repository browser.