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

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

Fix minor formatting/display issues with parameters. SASVIEW-1191

  • Property mode set to 100644
File size: 152.9 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        # set the cell to be non-editable
3086        item3.setFlags(item3.flags() ^ QtCore.Qt.ItemIsEditable)
3087
3088        # cell 4: max value
3089        item4 = QtGui.QStandardItem()
3090        # set the cell to be non-editable
3091        item4.setFlags(item4.flags() ^ QtCore.Qt.ItemIsEditable)
3092
3093        # cell 4: SLD button
3094        item5 = QtGui.QStandardItem()
3095        button = QtWidgets.QPushButton()
3096        button.setText("Show SLD Profile")
3097
3098        self._model_model.appendRow([item1, item2, item3, item4, item5])
3099
3100        # Beautify the row:  span columns 2-4
3101        shell_row = self._model_model.rowCount()
3102        shell_index = self._model_model.index(shell_row-1, 1)
3103        button_index = self._model_model.index(shell_row-1, 4)
3104
3105        self.lstParams.setIndexWidget(shell_index, func)
3106        self.lstParams.setIndexWidget(button_index, button)
3107        self._n_shells_row = shell_row - 1
3108
3109        # Get the default number of shells for the model
3110        kernel_pars = self.kernel_module._model_info.parameters.kernel_parameters
3111        shell_par = None
3112        for par in kernel_pars:
3113            parname = par.name
3114            if '[' in parname:
3115                 parname = parname[:parname.index('[')]
3116            if parname == param_name:
3117                shell_par = par
3118                break
3119        if shell_par is None:
3120            logger.error("Could not find %s in kernel parameters.", param_name)
3121            return
3122        default_shell_count = shell_par.default
3123        shell_min = 0
3124        shell_max = 0
3125        try:
3126            shell_min = int(shell_par.limits[0])
3127            shell_max = int(shell_par.limits[1])
3128        except IndexError as ex:
3129            # no info about limits
3130            pass
3131        except OverflowError:
3132            # Try to limit shell_par, if possible
3133            if float(shell_par.limits[1])==np.inf:
3134                shell_max = 9
3135            logging.warning("Limiting shell count to 9.")
3136        except Exception as ex:
3137            logging.error("Badly defined multiplicity: "+ str(ex))
3138            return
3139        # don't update the kernel here - this data is display only
3140        self._model_model.blockSignals(True)
3141        item3.setText(str(shell_min))
3142        item4.setText(str(shell_max))
3143        self._model_model.blockSignals(False)
3144
3145        ## Respond to index change
3146        #func.currentTextChanged.connect(self.modifyShellsInList)
3147
3148        # Respond to button press
3149        button.clicked.connect(self.onShowSLDProfile)
3150
3151        # Available range of shells displayed in the combobox
3152        func.addItems([str(i) for i in range(shell_min, shell_max+1)])
3153
3154        # Respond to index change
3155        func.currentTextChanged.connect(self.modifyShellsInList)
3156
3157        # Add default number of shells to the model
3158        func.setCurrentText(str(default_shell_count))
3159        self.modifyShellsInList(str(default_shell_count))
3160
3161    def modifyShellsInList(self, text):
3162        """
3163        Add/remove additional multishell parameters
3164        """
3165        # Find row location of the combobox
3166        first_row = self._n_shells_row + 1
3167        remove_rows = self._num_shell_params
3168        try:
3169            index = int(text)
3170        except ValueError:
3171            # bad text on the control!
3172            index = 0
3173            logger.error("Multiplicity incorrect! Setting to 0")
3174        self.kernel_module.multiplicity = index
3175        if remove_rows > 1:
3176            self._model_model.removeRows(first_row, remove_rows)
3177
3178        new_rows = FittingUtilities.addShellsToModel(
3179                self.model_parameters,
3180                self._model_model,
3181                index,
3182                first_row,
3183                self.lstParams)
3184
3185        self._num_shell_params = len(new_rows)
3186        self.current_shell_displayed = index
3187
3188        # Param values for existing shells were reset to default; force all changes into kernel module
3189        for row in new_rows:
3190            par = row[0].text()
3191            val = GuiUtils.toDouble(row[1].text())
3192            self.kernel_module.setParam(par, val)
3193
3194        # Change 'n' in the parameter model; also causes recalculation
3195        self._model_model.item(self._n_shells_row, 1).setText(str(index))
3196
3197        # Update relevant models
3198        self.setPolyModel()
3199        if self.canHaveMagnetism():
3200            self.setMagneticModel()
3201
3202    def onShowSLDProfile(self):
3203        """
3204        Show a quick plot of SLD profile
3205        """
3206        # get profile data
3207        try:
3208            x, y = self.kernel_module.getProfile()
3209        except TypeError:
3210            msg = "SLD profile calculation failed."
3211            logging.error(msg)
3212            return
3213
3214        y *= 1.0e6
3215        profile_data = Data1D(x=x, y=y)
3216        profile_data.name = "SLD"
3217        profile_data.scale = 'linear'
3218        profile_data.symbol = 'Line'
3219        profile_data.hide_error = True
3220        profile_data._xaxis = "R(\AA)"
3221        profile_data._yaxis = "SLD(10^{-6}\AA^{-2})"
3222
3223        plotter = PlotterWidget(self, quickplot=True)
3224        plotter.data = profile_data
3225        plotter.showLegend = True
3226        plotter.plot(hide_error=True, marker='-')
3227
3228        self.plot_widget = QtWidgets.QWidget()
3229        self.plot_widget.setWindowTitle("Scattering Length Density Profile")
3230        layout = QtWidgets.QVBoxLayout()
3231        layout.addWidget(plotter)
3232        self.plot_widget.setLayout(layout)
3233        self.plot_widget.show()
3234
3235    def setInteractiveElements(self, enabled=True):
3236        """
3237        Switch interactive GUI elements on/off
3238        """
3239        assert isinstance(enabled, bool)
3240
3241        self.lstParams.setEnabled(enabled)
3242        self.lstPoly.setEnabled(enabled)
3243        self.lstMagnetic.setEnabled(enabled)
3244
3245        self.cbCategory.setEnabled(enabled)
3246
3247        if enabled:
3248            # worry about original enablement of model and SF
3249            self.cbModel.setEnabled(self.enabled_cbmodel)
3250            self.cbStructureFactor.setEnabled(self.enabled_sfmodel)
3251        else:
3252            self.cbModel.setEnabled(enabled)
3253            self.cbStructureFactor.setEnabled(enabled)
3254
3255        self.cmdPlot.setEnabled(enabled)
3256
3257    def enableInteractiveElements(self):
3258        """
3259        Set buttion caption on fitting/calculate finish
3260        Enable the param table(s)
3261        """
3262        # Notify the user that fitting is available
3263        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
3264        self.cmdFit.setText("Fit")
3265        self.fit_started = False
3266        self.setInteractiveElements(True)
3267
3268    def disableInteractiveElements(self):
3269        """
3270        Set buttion caption on fitting/calculate start
3271        Disable the param table(s)
3272        """
3273        # Notify the user that fitting is being run
3274        # Allow for stopping the job
3275        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
3276        self.cmdFit.setText('Stop fit')
3277        self.setInteractiveElements(False)
3278
3279    def disableInteractiveElementsOnCalculate(self):
3280        """
3281        Set buttion caption on fitting/calculate start
3282        Disable the param table(s)
3283        """
3284        # Notify the user that fitting is being run
3285        # Allow for stopping the job
3286        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
3287        self.cmdFit.setText('Running...')
3288        self.setInteractiveElements(False)
3289
3290    def readFitPage(self, fp):
3291        """
3292        Read in state from a fitpage object and update GUI
3293        """
3294        assert isinstance(fp, FitPage)
3295        # Main tab info
3296        self.logic.data.filename = fp.filename
3297        self.data_is_loaded = fp.data_is_loaded
3298        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
3299        self.chkMagnetism.setCheckState(fp.is_magnetic)
3300        self.chk2DView.setCheckState(fp.is2D)
3301
3302        # Update the comboboxes
3303        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
3304        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
3305        if fp.current_factor:
3306            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
3307
3308        self.chi2 = fp.chi2
3309
3310        # Options tab
3311        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
3312        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
3313        self.npts = fp.fit_options[fp.NPTS]
3314        self.log_points = fp.fit_options[fp.LOG_POINTS]
3315        self.weighting = fp.fit_options[fp.WEIGHTING]
3316
3317        # Models
3318        self._model_model = fp.model_model
3319        self._poly_model = fp.poly_model
3320        self._magnet_model = fp.magnetism_model
3321
3322        # Resolution tab
3323        smearing = fp.smearing_options[fp.SMEARING_OPTION]
3324        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
3325        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
3326        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
3327        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
3328
3329        # TODO: add polidyspersity and magnetism
3330
3331    def saveToFitPage(self, fp):
3332        """
3333        Write current state to the given fitpage
3334        """
3335        assert isinstance(fp, FitPage)
3336
3337        # Main tab info
3338        fp.filename = self.logic.data.filename
3339        fp.data_is_loaded = self.data_is_loaded
3340        fp.is_polydisperse = self.chkPolydispersity.isChecked()
3341        fp.is_magnetic = self.chkMagnetism.isChecked()
3342        fp.is2D = self.chk2DView.isChecked()
3343        fp.data = self.data
3344
3345        # Use current models - they contain all the required parameters
3346        fp.model_model = self._model_model
3347        fp.poly_model = self._poly_model
3348        fp.magnetism_model = self._magnet_model
3349
3350        if self.cbCategory.currentIndex() != 0:
3351            fp.current_category = str(self.cbCategory.currentText())
3352            fp.current_model = str(self.cbModel.currentText())
3353
3354        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
3355            fp.current_factor = str(self.cbStructureFactor.currentText())
3356        else:
3357            fp.current_factor = ''
3358
3359        fp.chi2 = self.chi2
3360        fp.main_params_to_fit = self.main_params_to_fit
3361        fp.poly_params_to_fit = self.poly_params_to_fit
3362        fp.magnet_params_to_fit = self.magnet_params_to_fit
3363        fp.kernel_module = self.kernel_module
3364
3365        # Algorithm options
3366        # fp.algorithm = self.parent.fit_options.selected_id
3367
3368        # Options tab
3369        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
3370        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
3371        fp.fit_options[fp.NPTS] = self.npts
3372        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
3373        fp.fit_options[fp.LOG_POINTS] = self.log_points
3374        fp.fit_options[fp.WEIGHTING] = self.weighting
3375
3376        # Resolution tab
3377        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3378        fp.smearing_options[fp.SMEARING_OPTION] = smearing
3379        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
3380        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
3381        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
3382
3383        # TODO: add polidyspersity and magnetism
3384
3385    def updateUndo(self):
3386        """
3387        Create a new state page and add it to the stack
3388        """
3389        if self.undo_supported:
3390            self.pushFitPage(self.currentState())
3391
3392    def currentState(self):
3393        """
3394        Return fit page with current state
3395        """
3396        new_page = FitPage()
3397        self.saveToFitPage(new_page)
3398
3399        return new_page
3400
3401    def pushFitPage(self, new_page):
3402        """
3403        Add a new fit page object with current state
3404        """
3405        self.page_stack.append(new_page)
3406
3407    def popFitPage(self):
3408        """
3409        Remove top fit page from stack
3410        """
3411        if self.page_stack:
3412            self.page_stack.pop()
3413
3414    def getReport(self):
3415        """
3416        Create and return HTML report with parameters and charts
3417        """
3418        index = None
3419        if self.all_data:
3420            index = self.all_data[self.data_index]
3421        else:
3422            index = self.theory_item
3423        params = FittingUtilities.getStandardParam(self._model_model)
3424        report_logic = ReportPageLogic(self,
3425                                       kernel_module=self.kernel_module,
3426                                       data=self.data,
3427                                       index=index,
3428                                       params=params)
3429
3430        return report_logic.reportList()
3431
3432    def savePageState(self):
3433        """
3434        Create and serialize local PageState
3435        """
3436        filepath = self.saveAsAnalysisFile()
3437        if filepath is None or filepath == "":
3438            return
3439
3440        fitpage_state = self.getFitPage()
3441        fitpage_state += self.getFitModel()
3442
3443        with open(filepath, 'w') as statefile:
3444            for line in fitpage_state:
3445                statefile.write(str(line))
3446
3447        self.communicate.statusBarUpdateSignal.emit('Analysis saved.')
3448
3449    def saveAsAnalysisFile(self):
3450        """
3451        Show the save as... dialog and return the chosen filepath
3452        """
3453        default_name = "FitPage"+str(self.tab_id)+".fitv"
3454
3455        wildcard = "fitv files (*.fitv)"
3456        kwargs = {
3457            'caption'   : 'Save As',
3458            'directory' : default_name,
3459            'filter'    : wildcard,
3460            'parent'    : None,
3461        }
3462        # Query user for filename.
3463        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
3464        filename = filename_tuple[0]
3465        return filename
3466
3467    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
3468        """
3469        This is a callback method called from the CANSAS reader.
3470        We need the instance of this reader only for writing out a file,
3471        so there's nothing here.
3472        Until Load Analysis is implemented, that is.
3473        """
3474        pass
3475
3476    def loadPageState(self, pagestate=None):
3477        """
3478        Load the PageState object and update the current widget
3479        """
3480        filepath = self.loadAnalysisFile()
3481        if filepath is None or filepath == "":
3482            return
3483
3484        with open(filepath, 'r') as statefile:
3485            #column_data = [line.rstrip().split() for line in statefile.readlines()]
3486            lines = statefile.readlines()
3487
3488        # convert into list of lists
3489        pass
3490
3491    def loadAnalysisFile(self):
3492        """
3493        Called when the "Open Project" menu item chosen.
3494        """
3495        default_name = "FitPage"+str(self.tab_id)+".fitv"
3496        wildcard = "fitv files (*.fitv)"
3497        kwargs = {
3498            'caption'   : 'Open Analysis',
3499            'directory' : default_name,
3500            'filter'    : wildcard,
3501            'parent'    : self,
3502        }
3503        filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
3504        return filename
3505
3506    def onCopyToClipboard(self, format=None):
3507        """
3508        Copy current fitting parameters into the clipboard
3509        using requested formatting:
3510        plain, excel, latex
3511        """
3512        param_list = self.getFitParameters()
3513        if format=="":
3514            param_list = self.getFitPage()
3515            param_list += self.getFitModel()
3516            formatted_output = FittingUtilities.formatParameters(param_list)
3517        elif format == "Excel":
3518            formatted_output = FittingUtilities.formatParametersExcel(param_list[1:])
3519        elif format == "Latex":
3520            formatted_output = FittingUtilities.formatParametersLatex(param_list[1:])
3521        else:
3522            raise AttributeError("Bad parameter output format specifier.")
3523
3524        # Dump formatted_output to the clipboard
3525        cb = QtWidgets.QApplication.clipboard()
3526        cb.setText(formatted_output)
3527
3528    def getFitModel(self):
3529        """
3530        serializes combobox state
3531        """
3532        param_list = []
3533        model = str(self.cbModel.currentText())
3534        category = str(self.cbCategory.currentText())
3535        structure = str(self.cbStructureFactor.currentText())
3536        param_list.append(['fitpage_category', category])
3537        param_list.append(['fitpage_model', model])
3538        param_list.append(['fitpage_structure', structure])
3539
3540        return param_list
3541
3542    def getFitPage(self):
3543        """
3544        serializes full state of this fit page
3545        """
3546        # run a loop over all parameters and pull out
3547        # first - regular params
3548        param_list = self.getFitParameters()
3549
3550        param_list.append(['is_data', str(self.data_is_loaded)])
3551        if self.data_is_loaded:
3552            param_list.append(['data_id', str(self.logic.data.id)])
3553            param_list.append(['data_name', str(self.logic.data.filename)])
3554
3555        # option tab
3556        param_list.append(['q_range_min', str(self.q_range_min)])
3557        param_list.append(['q_range_max', str(self.q_range_max)])
3558        param_list.append(['q_weighting', str(self.weighting)])
3559        param_list.append(['weighting', str(self.options_widget.weighting)])
3560
3561        # resolution
3562        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3563        index = self.smearing_widget.cbSmearing.currentIndex()
3564        param_list.append(['smearing', str(index)])
3565        param_list.append(['smearing_min', str(smearing_min)])
3566        param_list.append(['smearing_max', str(smearing_max)])
3567
3568        # checkboxes, if required
3569        has_polydisp = self.chkPolydispersity.isChecked()
3570        has_magnetism = self.chkMagnetism.isChecked()
3571        has_chain = self.chkChainFit.isChecked()
3572        has_2D = self.chk2DView.isChecked()
3573        param_list.append(['polydisperse_params', str(has_polydisp)])
3574        param_list.append(['magnetic_params', str(has_magnetism)])
3575        param_list.append(['chainfit_params', str(has_chain)])
3576        param_list.append(['2D_params', str(has_2D)])
3577
3578        return param_list
3579
3580    def getFitParameters(self):
3581        """
3582        serializes current parameters
3583        """
3584        param_list = []
3585        param_list.append(['model_name', str(self.cbModel.currentText())])
3586
3587        def gatherParams(row):
3588            """
3589            Create list of main parameters based on _model_model
3590            """
3591            param_name = str(self._model_model.item(row, 0).text())
3592
3593            # Assure this is a parameter - must contain a checkbox
3594            if not self._model_model.item(row, 0).isCheckable():
3595                # maybe it is a combobox item (multiplicity)
3596                try:
3597                    index = self._model_model.index(row, 1)
3598                    widget = self.lstParams.indexWidget(index)
3599                    if widget is None:
3600                        return
3601                    if isinstance(widget, QtWidgets.QComboBox):
3602                        # find the index of the combobox
3603                        current_index = widget.currentIndex()
3604                        param_list.append([param_name, 'None', str(current_index)])
3605                except Exception as ex:
3606                    pass
3607                return
3608
3609            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3610            # Value of the parameter. In some cases this is the text of the combobox choice.
3611            param_value = str(self._model_model.item(row, 1).text())
3612            param_error = None
3613            param_min = None
3614            param_max = None
3615            column_offset = 0
3616            if self.has_error_column:
3617                column_offset = 1
3618                param_error = str(self._model_model.item(row, 1+column_offset).text())
3619            try:
3620                param_min = str(self._model_model.item(row, 2+column_offset).text())
3621                param_max = str(self._model_model.item(row, 3+column_offset).text())
3622            except:
3623                pass
3624
3625            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3626
3627        def gatherPolyParams(row):
3628            """
3629            Create list of polydisperse parameters based on _poly_model
3630            """
3631            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
3632            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3633            param_value = str(self._poly_model.item(row, 1).text())
3634            param_error = None
3635            column_offset = 0
3636            if self.has_poly_error_column:
3637                column_offset = 1
3638                param_error = str(self._poly_model.item(row, 1+column_offset).text())
3639            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
3640            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
3641            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
3642            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
3643            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
3644            index = self._poly_model.index(row, 6+column_offset)
3645            widget = self.lstPoly.indexWidget(index)
3646            if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3647                param_fun = widget.currentText()
3648            # width
3649            name = param_name+".width"
3650            param_list.append([name, param_checked, param_value, param_error,
3651                               param_min, param_max, param_npts, param_nsigs, param_fun])
3652
3653        def gatherMagnetParams(row):
3654            """
3655            Create list of magnetic parameters based on _magnet_model
3656            """
3657            param_name = str(self._magnet_model.item(row, 0).text())
3658            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3659            param_value = str(self._magnet_model.item(row, 1).text())
3660            param_error = None
3661            column_offset = 0
3662            if self.has_magnet_error_column:
3663                column_offset = 1
3664                param_error = str(self._magnet_model.item(row, 1+column_offset).text())
3665            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
3666            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
3667            param_list.append([param_name, param_checked, param_value,
3668                               param_error, param_min, param_max])
3669
3670        self.iterateOverModel(gatherParams)
3671        if self.chkPolydispersity.isChecked():
3672            self.iterateOverPolyModel(gatherPolyParams)
3673        if self.chkMagnetism.isChecked() and self.canHaveMagnetism():
3674            self.iterateOverMagnetModel(gatherMagnetParams)
3675
3676        if self.kernel_module.is_multiplicity_model:
3677            param_list.append(['multiplicity', str(self.kernel_module.multiplicity)])
3678
3679        return param_list
3680
3681    def onParameterPaste(self):
3682        """
3683        Use the clipboard to update fit state
3684        """
3685        # Check if the clipboard contains right stuff
3686        cb = QtWidgets.QApplication.clipboard()
3687        cb_text = cb.text()
3688
3689        context = {}
3690        lines = cb_text.split(':')
3691        if lines[0] != 'sasview_parameter_values':
3692            return False
3693
3694        # put the text into dictionary
3695        line_dict = {}
3696        for line in lines[1:]:
3697            content = line.split(',')
3698            if len(content) > 1:
3699                line_dict[content[0]] = content[1:]
3700
3701        model = line_dict['model_name'][0]
3702
3703        if 'model_name' not in line_dict.keys():
3704            return False
3705
3706        if 'multiplicity' in line_dict.keys():
3707            multip = int(line_dict['multiplicity'][0], 0)
3708            # reset the model with multiplicity, so further updates are saved
3709            if self.kernel_module.is_multiplicity_model:
3710                self.kernel_module.multiplicity=multip
3711                self.updateMultiplicityCombo(multip)
3712
3713        if 'polydisperse_params' in line_dict.keys():
3714            self.chkPolydispersity.setChecked(line_dict['polydisperse_params'][0]=='True')
3715        if 'magnetic_params' in line_dict.keys():
3716            self.chkMagnetism.setChecked(line_dict['magnetic_params'][0]=='True')
3717        if 'chainfit_params' in line_dict.keys():
3718            self.chkChainFit.setChecked(line_dict['chainfit_params'][0]=='True')
3719        if '2D_params' in line_dict.keys():
3720            self.chk2DView.setChecked(line_dict['2D_params'][0]=='True')
3721
3722        # Create the context dictionary for parameters
3723        context['model_name'] = model
3724        for key, value in line_dict.items():
3725            if len(value) > 2:
3726                context[key] = value
3727
3728        if str(self.cbModel.currentText()) != str(context['model_name']):
3729            msg = QtWidgets.QMessageBox()
3730            msg.setIcon(QtWidgets.QMessageBox.Information)
3731            msg.setText("The model in the clipboard is not the same as the currently loaded model. \
3732                         Not all parameters saved may paste correctly.")
3733            msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
3734            result = msg.exec_()
3735            if result == QtWidgets.QMessageBox.Ok:
3736                pass
3737            else:
3738                return
3739
3740        if 'smearing' in line_dict.keys():
3741            try:
3742                index = int(line_dict['smearing'][0])
3743                self.smearing_widget.cbSmearing.setCurrentIndex(index)
3744            except ValueError:
3745                pass
3746        if 'smearing_min' in line_dict.keys():
3747            try:
3748                self.smearing_widget.dq_l = float(line_dict['smearing_min'][0])
3749            except ValueError:
3750                pass
3751        if 'smearing_max' in line_dict.keys():
3752            try:
3753                self.smearing_widget.dq_r = float(line_dict['smearing_max'][0])
3754            except ValueError:
3755                pass
3756
3757        if 'q_range_max' in line_dict.keys():
3758            try:
3759                self.q_range_min = float(line_dict['q_range_min'][0])
3760                self.q_range_max = float(line_dict['q_range_max'][0])
3761            except ValueError:
3762                pass
3763        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
3764        try:
3765            button_id = int(line_dict['weighting'][0])
3766            for button in self.options_widget.weightingGroup.buttons():
3767                if abs(self.options_widget.weightingGroup.id(button)) == button_id+2:
3768                    button.setChecked(True)
3769                    break
3770        except ValueError:
3771            pass
3772
3773        self.updateFullModel(context)
3774        self.updateFullPolyModel(context)
3775        self.updateFullMagnetModel(context)
3776
3777    def updateMultiplicityCombo(self, multip):
3778        """
3779        Find and update the multiplicity combobox
3780        """
3781        index = self._model_model.index(self._n_shells_row, 1)
3782        widget = self.lstParams.indexWidget(index)
3783        if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3784            widget.setCurrentIndex(widget.findText(str(multip)))
3785        self.current_shell_displayed = multip
3786
3787    def updateFullModel(self, param_dict):
3788        """
3789        Update the model with new parameters
3790        """
3791        assert isinstance(param_dict, dict)
3792        if not dict:
3793            return
3794
3795        def updateFittedValues(row):
3796            # Utility function for main model update
3797            # internal so can use closure for param_dict
3798            param_name = str(self._model_model.item(row, 0).text())
3799            if param_name not in list(param_dict.keys()):
3800                return
3801            # Special case of combo box in the cell (multiplicity)
3802            param_line = param_dict[param_name]
3803            if len(param_line) == 1:
3804                # modify the shells value
3805                try:
3806                    combo_index = int(param_line[0])
3807                except ValueError:
3808                    # quietly pass
3809                    return
3810                index = self._model_model.index(row, 1)
3811                widget = self.lstParams.indexWidget(index)
3812                if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3813                    #widget.setCurrentIndex(combo_index)
3814                    return
3815            # checkbox state
3816            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3817            self._model_model.item(row, 0).setCheckState(param_checked)
3818
3819            # parameter value can be either just a value or text on the combobox
3820            param_text = param_dict[param_name][1]
3821            index = self._model_model.index(row, 1)
3822            widget = self.lstParams.indexWidget(index)
3823            if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3824                # Find the right index based on text
3825                combo_index = int(param_text, 0)
3826                widget.setCurrentIndex(combo_index)
3827            else:
3828                # modify the param value
3829                param_repr = GuiUtils.formatNumber(param_text, high=True)
3830                self._model_model.item(row, 1).setText(param_repr)
3831
3832            # Potentially the error column
3833            ioffset = 0
3834            joffset = 0
3835            if len(param_dict[param_name])>4:
3836                # error values are not editable - no need to update
3837                ioffset = 1
3838            if self.has_error_column:
3839                joffset = 1
3840            # min/max
3841            try:
3842                param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3843                self._model_model.item(row, 2+joffset).setText(param_repr)
3844                param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3845                self._model_model.item(row, 3+joffset).setText(param_repr)
3846            except:
3847                pass
3848
3849            self.setFocus()
3850
3851        self.iterateOverModel(updateFittedValues)
3852
3853    def updateFullPolyModel(self, param_dict):
3854        """
3855        Update the polydispersity model with new parameters, create the errors column
3856        """
3857        assert isinstance(param_dict, dict)
3858        if not dict:
3859            return
3860
3861        def updateFittedValues(row):
3862            # Utility function for main model update
3863            # internal so can use closure for param_dict
3864            if row >= self._poly_model.rowCount():
3865                return
3866            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
3867            if param_name not in list(param_dict.keys()):
3868                return
3869            # checkbox state
3870            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3871            self._poly_model.item(row,0).setCheckState(param_checked)
3872
3873            # modify the param value
3874            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3875            self._poly_model.item(row, 1).setText(param_repr)
3876
3877            # Potentially the error column
3878            ioffset = 0
3879            joffset = 0
3880            if len(param_dict[param_name])>7:
3881                ioffset = 1
3882            if self.has_poly_error_column:
3883                joffset = 1
3884            # min
3885            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3886            self._poly_model.item(row, 2+joffset).setText(param_repr)
3887            # max
3888            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3889            self._poly_model.item(row, 3+joffset).setText(param_repr)
3890            # Npts
3891            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
3892            self._poly_model.item(row, 4+joffset).setText(param_repr)
3893            # Nsigs
3894            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3895            self._poly_model.item(row, 5+joffset).setText(param_repr)
3896
3897            self.setFocus()
3898
3899        self.iterateOverPolyModel(updateFittedValues)
3900
3901    def updateFullMagnetModel(self, param_dict):
3902        """
3903        Update the magnetism model with new parameters, create the errors column
3904        """
3905        assert isinstance(param_dict, dict)
3906        if not dict:
3907            return
3908
3909        def updateFittedValues(row):
3910            # Utility function for main model update
3911            # internal so can use closure for param_dict
3912            if row >= self._magnet_model.rowCount():
3913                return
3914            param_name = str(self._magnet_model.item(row, 0).text()).rsplit()[-1]
3915            if param_name not in list(param_dict.keys()):
3916                return
3917            # checkbox state
3918            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3919            self._magnet_model.item(row,0).setCheckState(param_checked)
3920
3921            # modify the param value
3922            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3923            self._magnet_model.item(row, 1).setText(param_repr)
3924
3925            # Potentially the error column
3926            ioffset = 0
3927            joffset = 0
3928            if len(param_dict[param_name])>4:
3929                ioffset = 1
3930            if self.has_magnet_error_column:
3931                joffset = 1
3932            # min
3933            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3934            self._magnet_model.item(row, 2+joffset).setText(param_repr)
3935            # max
3936            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3937            self._magnet_model.item(row, 3+joffset).setText(param_repr)
3938
3939        self.iterateOverMagnetModel(updateFittedValues)
3940
3941    def getCurrentFitState(self, state=None):
3942        """
3943        Store current state for fit_page
3944        """
3945        # save model option
3946        #if self.model is not None:
3947        #    self.disp_list = self.getDispParamList()
3948        #    state.disp_list = copy.deepcopy(self.disp_list)
3949        #    #state.model = self.model.clone()
3950
3951        # Comboboxes
3952        state.categorycombobox = self.cbCategory.currentText()
3953        state.formfactorcombobox = self.cbModel.currentText()
3954        if self.cbStructureFactor.isEnabled():
3955            state.structurecombobox = self.cbStructureFactor.currentText()
3956        state.tcChi = self.chi2
3957
3958        state.enable2D = self.is2D
3959
3960        #state.weights = copy.deepcopy(self.weights)
3961        # save data
3962        state.data = copy.deepcopy(self.data)
3963
3964        # save plotting range
3965        state.qmin = self.q_range_min
3966        state.qmax = self.q_range_max
3967        state.npts = self.npts
3968
3969        #    self.state.enable_disp = self.enable_disp.GetValue()
3970        #    self.state.disable_disp = self.disable_disp.GetValue()
3971
3972        #    self.state.enable_smearer = \
3973        #                        copy.deepcopy(self.enable_smearer.GetValue())
3974        #    self.state.disable_smearer = \
3975        #                        copy.deepcopy(self.disable_smearer.GetValue())
3976
3977        #self.state.pinhole_smearer = \
3978        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
3979        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
3980        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
3981        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
3982        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
3983        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
3984
3985        p = self.model_parameters
3986        # save checkbutton state and txtcrtl values
3987        state.parameters = FittingUtilities.getStandardParam(self._model_model)
3988        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
3989
3990        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
3991        #self._copy_parameters_state(self.parameters, self.state.parameters)
3992        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
3993        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
3994
Note: See TracBrowser for help on using the repository browser.