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

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

Select the constrained parameter for fitting. SASVIEW-1198

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