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

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

Unify default qmin/qmax/npts. SASVIEW-1294

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