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

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

Merge branch 'ESS_GUI_project_save' into ESS_GUI

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