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

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

Added Edit to single page constraint SASVIEW-1043

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