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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 0c83303 was 0c83303, checked in by piotr, 21 months ago

Minor improvements after RH's review. SASVIEW-275

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