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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 287d356 was 287d356, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Don't wantonly cast parameter names into lower case. SASVIEW-1016

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