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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since f9be147 was f9be147, checked in by ibressler, 6 years ago

FittingWidget?.showTheoryPlot() should plot all matching items instead just one SASVIEW-1099

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