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

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