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

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 5aad7a5 was 5aad7a5, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Update residuals on param change as well. SASVIEW-1005

  • Property mode set to 100644
File size: 120.9 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        # Assure the control is active
961        if not self.cbModel.isEnabled():
962            return
963        # Empty combobox forced to be read
964        if not model:
965            return
966        # Reset structure factor
967        self.cbStructureFactor.setCurrentIndex(0)
968
969        # Reset parameters to fit
970        self.resetParametersToFit()
971        self.has_error_column = False
972        self.has_poly_error_column = False
973
974        self.respondToModelStructure(model=model, structure_factor=None)
975
976    def onSelectBatchFilename(self, data_index):
977        """
978        Update the logic based on the selected file in batch fitting
979        """
980        self.data_index = data_index
981        self.updateQRange()
982
983    def onSelectStructureFactor(self):
984        """
985        Select Structure Factor from list
986        """
987        model = str(self.cbModel.currentText())
988        category = str(self.cbCategory.currentText())
989        structure = str(self.cbStructureFactor.currentText())
990        if category == CATEGORY_STRUCTURE:
991            model = None
992
993        # Reset parameters to fit
994        self.resetParametersToFit()
995        self.has_error_column = False
996        self.has_poly_error_column = False
997
998        self.respondToModelStructure(model=model, structure_factor=structure)
999
1000    def resetParametersToFit(self):
1001        """
1002        Clears the list of parameters to be fitted
1003        """
1004        self.main_params_to_fit = []
1005        self.poly_params_to_fit = []
1006        self.magnet_params_to_fit = []
1007
1008    def onCustomModelChange(self):
1009        """
1010        Reload the custom model combobox
1011        """
1012        self.custom_models = self.customModels()
1013        self.readCustomCategoryInfo()
1014        # See if we need to update the combo in-place
1015        if self.cbCategory.currentText() != CATEGORY_CUSTOM: return
1016
1017        current_text = self.cbModel.currentText()
1018        self.cbModel.blockSignals(True)
1019        self.cbModel.clear()
1020        self.cbModel.blockSignals(False)
1021        self.enableModelCombo()
1022        self.disableStructureCombo()
1023        # Retrieve the list of models
1024        model_list = self.master_category_dict[CATEGORY_CUSTOM]
1025        # Populate the models combobox
1026        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
1027        new_index = self.cbModel.findText(current_text)
1028        if new_index != -1:
1029            self.cbModel.setCurrentIndex(self.cbModel.findText(current_text))
1030
1031    def onSelectionChanged(self):
1032        """
1033        React to parameter selection
1034        """
1035        rows = self.lstParams.selectionModel().selectedRows()
1036        # Clean previous messages
1037        self.communicate.statusBarUpdateSignal.emit("")
1038        if len(rows) == 1:
1039            # Show constraint, if present
1040            row = rows[0].row()
1041            if self.rowHasConstraint(row):
1042                func = self.getConstraintForRow(row).func
1043                if func is not None:
1044                    self.communicate.statusBarUpdateSignal.emit("Active constrain: "+func)
1045
1046    def replaceConstraintName(self, old_name, new_name=""):
1047        """
1048        Replace names of models in defined constraints
1049        """
1050        param_number = self._model_model.rowCount()
1051        # loop over parameters
1052        for row in range(param_number):
1053            if self.rowHasConstraint(row):
1054                func = self._model_model.item(row, 1).child(0).data().func
1055                if old_name in func:
1056                    new_func = func.replace(old_name, new_name)
1057                    self._model_model.item(row, 1).child(0).data().func = new_func
1058
1059    def isConstraintMultimodel(self, constraint):
1060        """
1061        Check if the constraint function text contains current model name
1062        """
1063        current_model_name = self.kernel_module.name
1064        if current_model_name in constraint:
1065            return False
1066        else:
1067            return True
1068
1069    def updateData(self):
1070        """
1071        Helper function for recalculation of data used in plotting
1072        """
1073        # Update the chart
1074        if self.data_is_loaded:
1075            self.cmdPlot.setText("Show Plot")
1076            self.calculateQGridForModel()
1077        else:
1078            self.cmdPlot.setText("Calculate")
1079            # Create default datasets if no data passed
1080            self.createDefaultDataset()
1081
1082    def respondToModelStructure(self, model=None, structure_factor=None):
1083        # Set enablement on calculate/plot
1084        self.cmdPlot.setEnabled(True)
1085
1086        # kernel parameters -> model_model
1087        self.SASModelToQModel(model, structure_factor)
1088
1089        # Update plot
1090        self.updateData()
1091
1092        # Update state stack
1093        self.updateUndo()
1094
1095        # Let others know
1096        self.newModelSignal.emit()
1097
1098    def onSelectCategory(self):
1099        """
1100        Select Category from list
1101        """
1102        category = self.cbCategory.currentText()
1103        # Check if the user chose "Choose category entry"
1104        if category == CATEGORY_DEFAULT:
1105            # if the previous category was not the default, keep it.
1106            # Otherwise, just return
1107            if self._previous_category_index != 0:
1108                # We need to block signals, or else state changes on perceived unchanged conditions
1109                self.cbCategory.blockSignals(True)
1110                self.cbCategory.setCurrentIndex(self._previous_category_index)
1111                self.cbCategory.blockSignals(False)
1112            return
1113
1114        if category == CATEGORY_STRUCTURE:
1115            self.disableModelCombo()
1116            self.enableStructureCombo()
1117            # set the index to 0
1118            self.cbStructureFactor.setCurrentIndex(0)
1119            self.model_parameters = None
1120            self._model_model.clear()
1121            return
1122
1123        # Safely clear and enable the model combo
1124        self.cbModel.blockSignals(True)
1125        self.cbModel.clear()
1126        self.cbModel.blockSignals(False)
1127        self.enableModelCombo()
1128        self.disableStructureCombo()
1129
1130        self._previous_category_index = self.cbCategory.currentIndex()
1131        # Retrieve the list of models
1132        model_list = self.master_category_dict[category]
1133        # Populate the models combobox
1134        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
1135
1136    def onPolyModelChange(self, item):
1137        """
1138        Callback method for updating the main model and sasmodel
1139        parameters with the GUI values in the polydispersity view
1140        """
1141        model_column = item.column()
1142        model_row = item.row()
1143        name_index = self._poly_model.index(model_row, 0)
1144        parameter_name = str(name_index.data()) # "distribution of sld" etc.
1145        if "istribution of" in parameter_name:
1146            # just the last word
1147            parameter_name = parameter_name.rsplit()[-1]
1148
1149        delegate = self.lstPoly.itemDelegate()
1150
1151        # Extract changed value.
1152        if model_column == delegate.poly_parameter:
1153            # Is the parameter checked for fitting?
1154            value = item.checkState()
1155            parameter_name = parameter_name + '.width'
1156            if value == QtCore.Qt.Checked:
1157                self.poly_params_to_fit.append(parameter_name)
1158            else:
1159                if parameter_name in self.poly_params_to_fit:
1160                    self.poly_params_to_fit.remove(parameter_name)
1161            self.cmdFit.setEnabled(self.haveParamsToFit())
1162
1163        elif model_column in [delegate.poly_min, delegate.poly_max]:
1164            try:
1165                value = GuiUtils.toDouble(item.text())
1166            except TypeError:
1167                # Can't be converted properly, bring back the old value and exit
1168                return
1169
1170            current_details = self.kernel_module.details[parameter_name]
1171            if self.has_poly_error_column:
1172                # err column changes the indexing
1173                current_details[model_column-2] = value
1174            else:
1175                current_details[model_column-1] = value
1176
1177        elif model_column == delegate.poly_function:
1178            # name of the function - just pass
1179            pass
1180
1181        else:
1182            try:
1183                value = GuiUtils.toDouble(item.text())
1184            except TypeError:
1185                # Can't be converted properly, bring back the old value and exit
1186                return
1187
1188            # Update the sasmodel
1189            # PD[ratio] -> width, npts -> npts, nsigs -> nsigmas
1190            self.kernel_module.setParam(parameter_name + '.' + delegate.columnDict()[model_column], value)
1191
1192            # Update plot
1193            self.updateData()
1194
1195        # update in param model
1196        if model_column in [delegate.poly_pd, delegate.poly_error, delegate.poly_min, delegate.poly_max]:
1197            row = self.getRowFromName(parameter_name)
1198            param_item = self._model_model.item(row)
1199            param_item.child(0).child(0, model_column).setText(item.text())
1200
1201    def onMagnetModelChange(self, item):
1202        """
1203        Callback method for updating the sasmodel magnetic parameters with the GUI values
1204        """
1205        model_column = item.column()
1206        model_row = item.row()
1207        name_index = self._magnet_model.index(model_row, 0)
1208        parameter_name = str(self._magnet_model.data(name_index))
1209
1210        if model_column == 0:
1211            value = item.checkState()
1212            if value == QtCore.Qt.Checked:
1213                self.magnet_params_to_fit.append(parameter_name)
1214            else:
1215                if parameter_name in self.magnet_params_to_fit:
1216                    self.magnet_params_to_fit.remove(parameter_name)
1217            self.cmdFit.setEnabled(self.haveParamsToFit())
1218            # Update state stack
1219            self.updateUndo()
1220            return
1221
1222        # Extract changed value.
1223        try:
1224            value = GuiUtils.toDouble(item.text())
1225        except TypeError:
1226            # Unparsable field
1227            return
1228
1229        property_index = self._magnet_model.headerData(1, model_column)-1 # Value, min, max, etc.
1230
1231        # Update the parameter value - note: this supports +/-inf as well
1232        self.kernel_module.params[parameter_name] = value
1233
1234        # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1235        self.kernel_module.details[parameter_name][property_index] = value
1236
1237        # Force the chart update when actual parameters changed
1238        if model_column == 1:
1239            self.recalculatePlotData()
1240
1241        # Update state stack
1242        self.updateUndo()
1243
1244    def onHelp(self):
1245        """
1246        Show the "Fitting" section of help
1247        """
1248        tree_location = "/user/qtgui/Perspectives/Fitting/"
1249
1250        # Actual file will depend on the current tab
1251        tab_id = self.tabFitting.currentIndex()
1252        helpfile = "fitting.html"
1253        if tab_id == 0:
1254            helpfile = "fitting_help.html"
1255        elif tab_id == 1:
1256            helpfile = "residuals_help.html"
1257        elif tab_id == 2:
1258            helpfile = "resolution.html"
1259        elif tab_id == 3:
1260            helpfile = "pd/polydispersity.html"
1261        elif tab_id == 4:
1262            helpfile = "magnetism/magnetism.html"
1263        help_location = tree_location + helpfile
1264
1265        self.showHelp(help_location)
1266
1267    def showHelp(self, url):
1268        """
1269        Calls parent's method for opening an HTML page
1270        """
1271        self.parent.showHelp(url)
1272
1273    def onDisplayMagneticAngles(self):
1274        """
1275        Display a simple image showing direction of magnetic angles
1276        """
1277        self.magneticAnglesWidget.show()
1278
1279    def onFit(self):
1280        """
1281        Perform fitting on the current data
1282        """
1283        if self.fit_started:
1284            self.stopFit()
1285            return
1286
1287        # initialize fitter constants
1288        fit_id = 0
1289        handler = None
1290        batch_inputs = {}
1291        batch_outputs = {}
1292        #---------------------------------
1293        if LocalConfig.USING_TWISTED:
1294            handler = None
1295            updater = None
1296        else:
1297            handler = ConsoleUpdate(parent=self.parent,
1298                                    manager=self,
1299                                    improvement_delta=0.1)
1300            updater = handler.update_fit
1301
1302        # Prepare the fitter object
1303        try:
1304            fitters, _ = self.prepareFitters()
1305        except ValueError as ex:
1306            # This should not happen! GUI explicitly forbids this situation
1307            self.communicate.statusBarUpdateSignal.emit(str(ex))
1308            return
1309
1310        # keep local copy of kernel parameters, as they will change during the update
1311        self.kernel_module_copy = copy.deepcopy(self.kernel_module)
1312
1313        # Create the fitting thread, based on the fitter
1314        completefn = self.batchFittingCompleted if self.is_batch_fitting else self.fittingCompleted
1315
1316        self.calc_fit = FitThread(handler=handler,
1317                            fn=fitters,
1318                            batch_inputs=batch_inputs,
1319                            batch_outputs=batch_outputs,
1320                            page_id=[[self.page_id]],
1321                            updatefn=updater,
1322                            completefn=completefn,
1323                            reset_flag=self.is_chain_fitting)
1324
1325        if LocalConfig.USING_TWISTED:
1326            # start the trhrhread with twisted
1327            calc_thread = threads.deferToThread(self.calc_fit.compute)
1328            calc_thread.addCallback(completefn)
1329            calc_thread.addErrback(self.fitFailed)
1330        else:
1331            # Use the old python threads + Queue
1332            self.calc_fit.queue()
1333            self.calc_fit.ready(2.5)
1334
1335        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
1336        self.fit_started = True
1337        # Disable some elements
1338        self.setFittingStarted()
1339
1340    def stopFit(self):
1341        """
1342        Attempt to stop the fitting thread
1343        """
1344        if self.calc_fit is None or not self.calc_fit.isrunning():
1345            return
1346        self.calc_fit.stop()
1347        #self.fit_started=False
1348        #re-enable the Fit button
1349        self.setFittingStopped()
1350
1351        msg = "Fitting cancelled."
1352        self.communicate.statusBarUpdateSignal.emit(msg)
1353
1354    def updateFit(self):
1355        """
1356        """
1357        print("UPDATE FIT")
1358        pass
1359
1360    def fitFailed(self, reason):
1361        """
1362        """
1363        self.setFittingStopped()
1364        msg = "Fitting failed with: "+ str(reason)
1365        self.communicate.statusBarUpdateSignal.emit(msg)
1366
1367    def batchFittingCompleted(self, result):
1368        """
1369        Send the finish message from calculate threads to main thread
1370        """
1371        if result is None:
1372            result = tuple()
1373        self.batchFittingFinishedSignal.emit(result)
1374
1375    def batchFitComplete(self, result):
1376        """
1377        Receive and display batch fitting results
1378        """
1379        #re-enable the Fit button
1380        self.setFittingStopped()
1381
1382        if len(result) == 0:
1383            msg = "Fitting failed."
1384            self.communicate.statusBarUpdateSignal.emit(msg)
1385            return
1386
1387        # Show the grid panel
1388        self.communicate.sendDataToGridSignal.emit(result[0])
1389
1390        elapsed = result[1]
1391        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
1392        self.communicate.statusBarUpdateSignal.emit(msg)
1393
1394        # Run over the list of results and update the items
1395        for res_index, res_list in enumerate(result[0]):
1396            # results
1397            res = res_list[0]
1398            param_dict = self.paramDictFromResults(res)
1399
1400            # create local kernel_module
1401            kernel_module = FittingUtilities.updateKernelWithResults(self.kernel_module, param_dict)
1402            # pull out current data
1403            data = self._logic[res_index].data
1404
1405            # Switch indexes
1406            self.onSelectBatchFilename(res_index)
1407
1408            method = self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1409            self.calculateQGridForModelExt(data=data, model=kernel_module, completefn=method, use_threads=False)
1410
1411        # Restore original kernel_module, so subsequent fits on the same model don't pick up the new params
1412        if self.kernel_module is not None:
1413            self.kernel_module = copy.deepcopy(self.kernel_module_copy)
1414
1415    def paramDictFromResults(self, results):
1416        """
1417        Given the fit results structure, pull out optimized parameters and return them as nicely
1418        formatted dict
1419        """
1420        if results.fitness is None or \
1421            not np.isfinite(results.fitness) or \
1422            np.any(results.pvec is None) or \
1423            not np.all(np.isfinite(results.pvec)):
1424            msg = "Fitting did not converge!"
1425            self.communicate.statusBarUpdateSignal.emit(msg)
1426            msg += results.mesg
1427            logging.error(msg)
1428            return
1429
1430        param_list = results.param_list # ['radius', 'radius.width']
1431        param_values = results.pvec     # array([ 0.36221662,  0.0146783 ])
1432        param_stderr = results.stderr   # array([ 1.71293015,  1.71294233])
1433        params_and_errors = list(zip(param_values, param_stderr))
1434        param_dict = dict(zip(param_list, params_and_errors))
1435
1436        return param_dict
1437
1438    def fittingCompleted(self, result):
1439        """
1440        Send the finish message from calculate threads to main thread
1441        """
1442        if result is None:
1443            result = tuple()
1444        self.fittingFinishedSignal.emit(result)
1445
1446    def fitComplete(self, result):
1447        """
1448        Receive and display fitting results
1449        "result" is a tuple of actual result list and the fit time in seconds
1450        """
1451        #re-enable the Fit button
1452        self.setFittingStopped()
1453
1454        if len(result) == 0:
1455            msg = "Fitting failed."
1456            self.communicate.statusBarUpdateSignal.emit(msg)
1457            return
1458
1459        res_list = result[0][0]
1460        res = res_list[0]
1461        self.chi2 = res.fitness
1462        param_dict = self.paramDictFromResults(res)
1463
1464        if param_dict is None:
1465            return
1466
1467        elapsed = result[1]
1468        if self.calc_fit._interrupting:
1469            msg = "Fitting cancelled by user after: %s s." % GuiUtils.formatNumber(elapsed)
1470            logging.warning("\n"+msg+"\n")
1471        else:
1472            msg = "Fitting completed successfully in: %s s." % GuiUtils.formatNumber(elapsed)
1473        self.communicate.statusBarUpdateSignal.emit(msg)
1474
1475        # Dictionary of fitted parameter: value, error
1476        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
1477        self.updateModelFromList(param_dict)
1478
1479        self.updatePolyModelFromList(param_dict)
1480
1481        self.updateMagnetModelFromList(param_dict)
1482
1483        # update charts
1484        self.onPlot()
1485
1486        # Read only value - we can get away by just printing it here
1487        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
1488        self.lblChi2Value.setText(chi2_repr)
1489
1490    def prepareFitters(self, fitter=None, fit_id=0):
1491        """
1492        Prepare the Fitter object for use in fitting
1493        """
1494        # fitter = None -> single/batch fitting
1495        # fitter = Fit() -> simultaneous fitting
1496
1497        # Data going in
1498        data = self.logic.data
1499        model = self.kernel_module
1500        qmin = self.q_range_min
1501        qmax = self.q_range_max
1502
1503        params_to_fit = self.main_params_to_fit
1504        if self.chkPolydispersity.isChecked():
1505            params_to_fit += self.poly_params_to_fit
1506        if self.chkMagnetism.isChecked():
1507            params_to_fit += self.magnet_params_to_fit
1508        if not params_to_fit:
1509            raise ValueError('Fitting requires at least one parameter to optimize.')
1510
1511        # Potential smearing added
1512        # Remember that smearing_min/max can be None ->
1513        # deal with it until Python gets discriminated unions
1514        self.addWeightingToData(data)
1515
1516        # Get the constraints.
1517        constraints = self.getComplexConstraintsForModel()
1518        if fitter is None:
1519            # For single fits - check for inter-model constraints
1520            constraints = self.getConstraintsForFitting()
1521
1522        smearer = self.smearing_widget.smearer()
1523        handler = None
1524        batch_inputs = {}
1525        batch_outputs = {}
1526
1527        fitters = []
1528        for fit_index in self.all_data:
1529            fitter_single = Fit() if fitter is None else fitter
1530            data = GuiUtils.dataFromItem(fit_index)
1531            # Potential weights added directly to data
1532            self.addWeightingToData(data)
1533            try:
1534                fitter_single.set_model(model, fit_id, params_to_fit, data=data,
1535                             constraints=constraints)
1536            except ValueError as ex:
1537                raise ValueError("Setting model parameters failed with: %s" % ex)
1538
1539            qmin, qmax, _ = self.logic.computeRangeFromData(data)
1540            fitter_single.set_data(data=data, id=fit_id, smearer=smearer, qmin=qmin,
1541                            qmax=qmax)
1542            fitter_single.select_problem_for_fit(id=fit_id, value=1)
1543            if fitter is None:
1544                # Assign id to the new fitter only
1545                fitter_single.fitter_id = [self.page_id]
1546            fit_id += 1
1547            fitters.append(fitter_single)
1548
1549        return fitters, fit_id
1550
1551    def iterateOverModel(self, func):
1552        """
1553        Take func and throw it inside the model row loop
1554        """
1555        for row_i in range(self._model_model.rowCount()):
1556            func(row_i)
1557
1558    def updateModelFromList(self, param_dict):
1559        """
1560        Update the model with new parameters, create the errors column
1561        """
1562        assert isinstance(param_dict, dict)
1563        if not dict:
1564            return
1565
1566        def updateFittedValues(row):
1567            # Utility function for main model update
1568            # internal so can use closure for param_dict
1569            param_name = str(self._model_model.item(row, 0).text())
1570            if param_name not in list(param_dict.keys()):
1571                return
1572            # modify the param value
1573            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1574            self._model_model.item(row, 1).setText(param_repr)
1575            if self.has_error_column:
1576                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1577                self._model_model.item(row, 2).setText(error_repr)
1578
1579        def updatePolyValues(row):
1580            # Utility function for updateof polydispersity part of the main model
1581            param_name = str(self._model_model.item(row, 0).text())+'.width'
1582            if param_name not in list(param_dict.keys()):
1583                return
1584            # modify the param value
1585            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1586            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
1587            # modify the param error
1588            if self.has_error_column:
1589                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1590                self._model_model.item(row, 0).child(0).child(0,2).setText(error_repr)
1591
1592        def createErrorColumn(row):
1593            # Utility function for error column update
1594            item = QtGui.QStandardItem()
1595            def createItem(param_name):
1596                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1597                item.setText(error_repr)
1598            def curr_param():
1599                return str(self._model_model.item(row, 0).text())
1600
1601            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1602
1603            error_column.append(item)
1604
1605        def createPolyErrorColumn(row):
1606            # Utility function for error column update in the polydispersity sub-rows
1607            # NOTE: only creates empty items; updatePolyValues adds the error value
1608            item = self._model_model.item(row, 0)
1609            if not item.hasChildren():
1610                return
1611            poly_item = item.child(0)
1612            if not poly_item.hasChildren():
1613                return
1614            poly_item.insertColumn(2, [QtGui.QStandardItem("")])
1615
1616        # block signals temporarily, so we don't end up
1617        # updating charts with every single model change on the end of fitting
1618        self._model_model.blockSignals(True)
1619
1620        if not self.has_error_column:
1621            # create top-level error column
1622            error_column = []
1623            self.lstParams.itemDelegate().addErrorColumn()
1624            self.iterateOverModel(createErrorColumn)
1625
1626            # we need to enable signals for this, otherwise the final column mysteriously disappears (don't ask, I don't
1627            # know)
1628            self._model_model.blockSignals(False)
1629            self._model_model.insertColumn(2, error_column)
1630            self._model_model.blockSignals(True)
1631
1632            FittingUtilities.addErrorHeadersToModel(self._model_model)
1633
1634            # create error column in polydispersity sub-rows
1635            self.iterateOverModel(createPolyErrorColumn)
1636
1637            self.has_error_column = True
1638
1639        self.iterateOverModel(updateFittedValues)
1640        self.iterateOverModel(updatePolyValues)
1641
1642        self._model_model.blockSignals(False)
1643
1644        # Adjust the table cells width.
1645        # TODO: find a way to dynamically adjust column width while resized expanding
1646        self.lstParams.resizeColumnToContents(0)
1647        self.lstParams.resizeColumnToContents(4)
1648        self.lstParams.resizeColumnToContents(5)
1649        self.lstParams.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
1650
1651    def iterateOverPolyModel(self, func):
1652        """
1653        Take func and throw it inside the poly model row loop
1654        """
1655        for row_i in range(self._poly_model.rowCount()):
1656            func(row_i)
1657
1658    def updatePolyModelFromList(self, param_dict):
1659        """
1660        Update the polydispersity model with new parameters, create the errors column
1661        """
1662        assert isinstance(param_dict, dict)
1663        if not dict:
1664            return
1665
1666        def updateFittedValues(row_i):
1667            # Utility function for main model update
1668            # internal so can use closure for param_dict
1669            if row_i >= self._poly_model.rowCount():
1670                return
1671            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1672            if param_name not in list(param_dict.keys()):
1673                return
1674            # modify the param value
1675            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1676            self._poly_model.item(row_i, 1).setText(param_repr)
1677            if self.has_poly_error_column:
1678                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1679                self._poly_model.item(row_i, 2).setText(error_repr)
1680
1681
1682        def createErrorColumn(row_i):
1683            # Utility function for error column update
1684            if row_i >= self._poly_model.rowCount():
1685                return
1686            item = QtGui.QStandardItem()
1687
1688            def createItem(param_name):
1689                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1690                item.setText(error_repr)
1691
1692            def poly_param():
1693                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1694
1695            [createItem(param_name) for param_name in list(param_dict.keys()) if poly_param() == param_name]
1696
1697            error_column.append(item)
1698
1699        # block signals temporarily, so we don't end up
1700        # updating charts with every single model change on the end of fitting
1701        self._poly_model.blockSignals(True)
1702        self.iterateOverPolyModel(updateFittedValues)
1703        self._poly_model.blockSignals(False)
1704
1705        if self.has_poly_error_column:
1706            return
1707
1708        self.lstPoly.itemDelegate().addErrorColumn()
1709        error_column = []
1710        self.iterateOverPolyModel(createErrorColumn)
1711
1712        # switch off reponse to model change
1713        self._poly_model.blockSignals(True)
1714        self._poly_model.insertColumn(2, error_column)
1715        self._poly_model.blockSignals(False)
1716        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
1717
1718        self.has_poly_error_column = True
1719
1720    def iterateOverMagnetModel(self, func):
1721        """
1722        Take func and throw it inside the magnet model row loop
1723        """
1724        for row_i in range(self._magnet_model.rowCount()):
1725            func(row_i)
1726
1727    def updateMagnetModelFromList(self, param_dict):
1728        """
1729        Update the magnetic model with new parameters, create the errors column
1730        """
1731        assert isinstance(param_dict, dict)
1732        if not dict:
1733            return
1734        if self._magnet_model.rowCount() == 0:
1735            return
1736
1737        def updateFittedValues(row):
1738            # Utility function for main model update
1739            # internal so can use closure for param_dict
1740            if self._magnet_model.item(row, 0) is None:
1741                return
1742            param_name = str(self._magnet_model.item(row, 0).text())
1743            if param_name not in list(param_dict.keys()):
1744                return
1745            # modify the param value
1746            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1747            self._magnet_model.item(row, 1).setText(param_repr)
1748            if self.has_magnet_error_column:
1749                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1750                self._magnet_model.item(row, 2).setText(error_repr)
1751
1752        def createErrorColumn(row):
1753            # Utility function for error column update
1754            item = QtGui.QStandardItem()
1755            def createItem(param_name):
1756                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1757                item.setText(error_repr)
1758            def curr_param():
1759                return str(self._magnet_model.item(row, 0).text())
1760
1761            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1762
1763            error_column.append(item)
1764
1765        # block signals temporarily, so we don't end up
1766        # updating charts with every single model change on the end of fitting
1767        self._magnet_model.blockSignals(True)
1768        self.iterateOverMagnetModel(updateFittedValues)
1769        self._magnet_model.blockSignals(False)
1770
1771        if self.has_magnet_error_column:
1772            return
1773
1774        self.lstMagnetic.itemDelegate().addErrorColumn()
1775        error_column = []
1776        self.iterateOverMagnetModel(createErrorColumn)
1777
1778        # switch off reponse to model change
1779        self._magnet_model.blockSignals(True)
1780        self._magnet_model.insertColumn(2, error_column)
1781        self._magnet_model.blockSignals(False)
1782        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1783
1784        self.has_magnet_error_column = True
1785
1786    def onPlot(self):
1787        """
1788        Plot the current set of data
1789        """
1790        # Regardless of previous state, this should now be `plot show` functionality only
1791        self.cmdPlot.setText("Show Plot")
1792        # Force data recalculation so existing charts are updated
1793        self.recalculatePlotData()
1794        self.showPlot()
1795
1796    def onSmearingOptionsUpdate(self):
1797        """
1798        React to changes in the smearing widget
1799        """
1800        self.calculateQGridForModel()
1801
1802    def recalculatePlotData(self):
1803        """
1804        Generate a new dataset for model
1805        """
1806        if not self.data_is_loaded:
1807            self.createDefaultDataset()
1808        self.calculateQGridForModel()
1809
1810    def showPlot(self):
1811        """
1812        Show the current plot in MPL
1813        """
1814        # Show the chart if ready
1815        data_to_show = self.data if self.data_is_loaded else self.model_data
1816        if data_to_show is not None:
1817            self.communicate.plotRequestedSignal.emit([data_to_show])
1818
1819    def onOptionsUpdate(self):
1820        """
1821        Update local option values and replot
1822        """
1823        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1824            self.options_widget.state()
1825        # set Q range labels on the main tab
1826        self.lblMinRangeDef.setText(str(self.q_range_min))
1827        self.lblMaxRangeDef.setText(str(self.q_range_max))
1828        self.recalculatePlotData()
1829
1830    def setDefaultStructureCombo(self):
1831        """
1832        Fill in the structure factors combo box with defaults
1833        """
1834        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1835        factors = [factor[0] for factor in structure_factor_list]
1836        factors.insert(0, STRUCTURE_DEFAULT)
1837        self.cbStructureFactor.clear()
1838        self.cbStructureFactor.addItems(sorted(factors))
1839
1840    def createDefaultDataset(self):
1841        """
1842        Generate default Dataset 1D/2D for the given model
1843        """
1844        # Create default datasets if no data passed
1845        if self.is2D:
1846            qmax = self.q_range_max/np.sqrt(2)
1847            qstep = self.npts
1848            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
1849            return
1850        elif self.log_points:
1851            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
1852            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
1853            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
1854        else:
1855            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
1856                                   num=self.npts, endpoint=True)
1857        self.logic.createDefault1dData(interval, self.tab_id)
1858
1859    def readCategoryInfo(self):
1860        """
1861        Reads the categories in from file
1862        """
1863        self.master_category_dict = defaultdict(list)
1864        self.by_model_dict = defaultdict(list)
1865        self.model_enabled_dict = defaultdict(bool)
1866
1867        categorization_file = CategoryInstaller.get_user_file()
1868        if not os.path.isfile(categorization_file):
1869            categorization_file = CategoryInstaller.get_default_file()
1870        with open(categorization_file, 'rb') as cat_file:
1871            self.master_category_dict = json.load(cat_file)
1872            self.regenerateModelDict()
1873
1874        # Load the model dict
1875        models = load_standard_models()
1876        for model in models:
1877            self.models[model.name] = model
1878
1879        self.readCustomCategoryInfo()
1880
1881    def readCustomCategoryInfo(self):
1882        """
1883        Reads the custom model category
1884        """
1885        #Looking for plugins
1886        self.plugins = list(self.custom_models.values())
1887        plugin_list = []
1888        for name, plug in self.custom_models.items():
1889            self.models[name] = plug
1890            plugin_list.append([name, True])
1891        self.master_category_dict[CATEGORY_CUSTOM] = plugin_list
1892
1893    def regenerateModelDict(self):
1894        """
1895        Regenerates self.by_model_dict which has each model name as the
1896        key and the list of categories belonging to that model
1897        along with the enabled mapping
1898        """
1899        self.by_model_dict = defaultdict(list)
1900        for category in self.master_category_dict:
1901            for (model, enabled) in self.master_category_dict[category]:
1902                self.by_model_dict[model].append(category)
1903                self.model_enabled_dict[model] = enabled
1904
1905    def addBackgroundToModel(self, model):
1906        """
1907        Adds background parameter with default values to the model
1908        """
1909        assert isinstance(model, QtGui.QStandardItemModel)
1910        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
1911        FittingUtilities.addCheckedListToModel(model, checked_list)
1912        last_row = model.rowCount()-1
1913        model.item(last_row, 0).setEditable(False)
1914        model.item(last_row, 4).setEditable(False)
1915
1916    def addScaleToModel(self, model):
1917        """
1918        Adds scale parameter with default values to the model
1919        """
1920        assert isinstance(model, QtGui.QStandardItemModel)
1921        checked_list = ['scale', '1.0', '0.0', 'inf', '']
1922        FittingUtilities.addCheckedListToModel(model, checked_list)
1923        last_row = model.rowCount()-1
1924        model.item(last_row, 0).setEditable(False)
1925        model.item(last_row, 4).setEditable(False)
1926
1927    def addWeightingToData(self, data):
1928        """
1929        Adds weighting contribution to fitting data
1930        """
1931        # Send original data for weighting
1932        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
1933        if self.is2D:
1934            data.err_data = weight
1935        else:
1936            data.dy = weight
1937        pass
1938
1939    def updateQRange(self):
1940        """
1941        Updates Q Range display
1942        """
1943        if self.data_is_loaded:
1944            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
1945        # set Q range labels on the main tab
1946        self.lblMinRangeDef.setText(str(self.q_range_min))
1947        self.lblMaxRangeDef.setText(str(self.q_range_max))
1948        # set Q range labels on the options tab
1949        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
1950
1951    def SASModelToQModel(self, model_name, structure_factor=None):
1952        """
1953        Setting model parameters into table based on selected category
1954        """
1955        # Crete/overwrite model items
1956        self._model_model.clear()
1957
1958        # First, add parameters from the main model
1959        if model_name is not None:
1960            self.fromModelToQModel(model_name)
1961
1962        # Then, add structure factor derived parameters
1963        if structure_factor is not None and structure_factor != "None":
1964            if model_name is None:
1965                # Instantiate the current sasmodel for SF-only models
1966                self.kernel_module = self.models[structure_factor]()
1967            self.fromStructureFactorToQModel(structure_factor)
1968        else:
1969            # Allow the SF combobox visibility for the given sasmodel
1970            self.enableStructureFactorControl(structure_factor)
1971
1972        # Then, add multishells
1973        if model_name is not None:
1974            # Multishell models need additional treatment
1975            self.addExtraShells()
1976
1977        # Add polydispersity to the model
1978        self.setPolyModel()
1979        # Add magnetic parameters to the model
1980        self.setMagneticModel()
1981
1982        # Adjust the table cells width
1983        self.lstParams.resizeColumnToContents(0)
1984        self.lstParams.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
1985
1986        # Now we claim the model has been loaded
1987        self.model_is_loaded = True
1988        # Change the model name to a monicker
1989        self.kernel_module.name = self.modelName()
1990        # Update the smearing tab
1991        self.smearing_widget.updateKernelModel(kernel_model=self.kernel_module)
1992
1993        # (Re)-create headers
1994        FittingUtilities.addHeadersToModel(self._model_model)
1995        self.lstParams.header().setFont(self.boldFont)
1996
1997        # Update Q Ranges
1998        self.updateQRange()
1999
2000    def fromModelToQModel(self, model_name):
2001        """
2002        Setting model parameters into QStandardItemModel based on selected _model_
2003        """
2004        name = model_name
2005        kernel_module = None
2006        if self.cbCategory.currentText() == CATEGORY_CUSTOM:
2007            # custom kernel load requires full path
2008            name = os.path.join(ModelUtilities.find_plugins_dir(), model_name+".py")
2009        try:
2010            kernel_module = generate.load_kernel_module(name)
2011        except ModuleNotFoundError as ex:
2012            pass
2013
2014        if kernel_module is None:
2015            # mismatch between "name" attribute and actual filename.
2016            curr_model = self.models[model_name]
2017            name, _ = os.path.splitext(os.path.basename(curr_model.filename))
2018            try:
2019                kernel_module = generate.load_kernel_module(name)
2020            except ModuleNotFoundError as ex:
2021                logging.error("Can't find the model "+ str(ex))
2022                return
2023
2024        if hasattr(kernel_module, 'parameters'):
2025            # built-in and custom models
2026            self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
2027
2028        elif hasattr(kernel_module, 'model_info'):
2029            # for sum/multiply models
2030            self.model_parameters = kernel_module.model_info.parameters
2031
2032        elif hasattr(kernel_module, 'Model') and hasattr(kernel_module.Model, "_model_info"):
2033            # this probably won't work if there's no model_info, but just in case
2034            self.model_parameters = kernel_module.Model._model_info.parameters
2035        else:
2036            # no parameters - default to blank table
2037            msg = "No parameters found in model '{}'.".format(model_name)
2038            logger.warning(msg)
2039            self.model_parameters = modelinfo.ParameterTable([])
2040
2041        # Instantiate the current sasmodel
2042        self.kernel_module = self.models[model_name]()
2043
2044        # Explicitly add scale and background with default values
2045        temp_undo_state = self.undo_supported
2046        self.undo_supported = False
2047        self.addScaleToModel(self._model_model)
2048        self.addBackgroundToModel(self._model_model)
2049        self.undo_supported = temp_undo_state
2050
2051        self.shell_names = self.shellNamesList()
2052
2053        # Update the QModel
2054        new_rows = FittingUtilities.addParametersToModel(self.model_parameters, self.kernel_module, self.is2D)
2055
2056        for row in new_rows:
2057            self._model_model.appendRow(row)
2058        # Update the counter used for multishell display
2059        self._last_model_row = self._model_model.rowCount()
2060
2061    def fromStructureFactorToQModel(self, structure_factor):
2062        """
2063        Setting model parameters into QStandardItemModel based on selected _structure factor_
2064        """
2065        structure_module = generate.load_kernel_module(structure_factor)
2066        structure_parameters = modelinfo.make_parameter_table(getattr(structure_module, 'parameters', []))
2067
2068        structure_kernel = self.models[structure_factor]()
2069        form_kernel = self.kernel_module
2070
2071        self.kernel_module = MultiplicationModel(form_kernel, structure_kernel)
2072
2073        new_rows = FittingUtilities.addSimpleParametersToModel(structure_parameters, self.is2D)
2074        for row in new_rows:
2075            self._model_model.appendRow(row)
2076            # disable fitting of parameters not listed in self.kernel_module (probably radius_effective)
2077            if row[0].text() not in self.kernel_module.params.keys():
2078                row_num = self._model_model.rowCount() - 1
2079                FittingUtilities.markParameterDisabled(self._model_model, row_num)
2080
2081        # Update the counter used for multishell display
2082        self._last_model_row = self._model_model.rowCount()
2083
2084    def haveParamsToFit(self):
2085        """
2086        Finds out if there are any parameters ready to be fitted
2087        """
2088        return (self.main_params_to_fit!=[]
2089                or self.poly_params_to_fit!=[]
2090                or self.magnet_params_to_fit != []) and \
2091                self.logic.data_is_loaded
2092
2093    def onMainParamsChange(self, item):
2094        """
2095        Callback method for updating the sasmodel parameters with the GUI values
2096        """
2097        model_column = item.column()
2098
2099        if model_column == 0:
2100            self.checkboxSelected(item)
2101            self.cmdFit.setEnabled(self.haveParamsToFit())
2102            # Update state stack
2103            self.updateUndo()
2104            return
2105
2106        model_row = item.row()
2107        name_index = self._model_model.index(model_row, 0)
2108
2109        # Extract changed value.
2110        try:
2111            value = GuiUtils.toDouble(item.text())
2112        except TypeError:
2113            # Unparsable field
2114            return
2115
2116        parameter_name = str(self._model_model.data(name_index)) # sld, background etc.
2117
2118        # Update the parameter value - note: this supports +/-inf as well
2119        self.kernel_module.params[parameter_name] = value
2120
2121        # Update the parameter value - note: this supports +/-inf as well
2122        param_column = self.lstParams.itemDelegate().param_value
2123        min_column = self.lstParams.itemDelegate().param_min
2124        max_column = self.lstParams.itemDelegate().param_max
2125        if model_column == param_column:
2126            self.kernel_module.setParam(parameter_name, value)
2127        elif model_column == min_column:
2128            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
2129            self.kernel_module.details[parameter_name][1] = value
2130        elif model_column == max_column:
2131            self.kernel_module.details[parameter_name][2] = value
2132        else:
2133            # don't update the chart
2134            return
2135
2136        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
2137        # TODO: multishell params in self.kernel_module.details[??] = value
2138
2139        # Force the chart update when actual parameters changed
2140        if model_column == 1:
2141            self.recalculatePlotData()
2142
2143        # Update state stack
2144        self.updateUndo()
2145
2146    def isCheckable(self, row):
2147        return self._model_model.item(row, 0).isCheckable()
2148
2149    def checkboxSelected(self, item):
2150        # Assure we're dealing with checkboxes
2151        if not item.isCheckable():
2152            return
2153        status = item.checkState()
2154
2155        # If multiple rows selected - toggle all of them, filtering uncheckable
2156        # Switch off signaling from the model to avoid recursion
2157        self._model_model.blockSignals(True)
2158        # Convert to proper indices and set requested enablement
2159        self.setParameterSelection(status)
2160        self._model_model.blockSignals(False)
2161
2162        # update the list of parameters to fit
2163        self.main_params_to_fit = self.checkedListFromModel(self._model_model)
2164
2165    def checkedListFromModel(self, model):
2166        """
2167        Returns list of checked parameters for given model
2168        """
2169        def isChecked(row):
2170            return model.item(row, 0).checkState() == QtCore.Qt.Checked
2171
2172        return [str(model.item(row_index, 0).text())
2173                for row_index in range(model.rowCount())
2174                if isChecked(row_index)]
2175
2176    def createNewIndex(self, fitted_data):
2177        """
2178        Create a model or theory index with passed Data1D/Data2D
2179        """
2180        if self.data_is_loaded:
2181            if not fitted_data.name:
2182                name = self.nameForFittedData(self.data.filename)
2183                fitted_data.title = name
2184                fitted_data.name = name
2185                fitted_data.filename = name
2186                fitted_data.symbol = "Line"
2187            self.updateModelIndex(fitted_data)
2188        else:
2189            if not fitted_data.name:
2190                name = self.nameForFittedData(self.kernel_module.id)
2191            else:
2192                name = fitted_data.name
2193            fitted_data.title = name
2194            fitted_data.filename = name
2195            fitted_data.symbol = "Line"
2196            self.createTheoryIndex(fitted_data)
2197
2198    def updateModelIndex(self, fitted_data):
2199        """
2200        Update a QStandardModelIndex containing model data
2201        """
2202        name = self.nameFromData(fitted_data)
2203        # Make this a line if no other defined
2204        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
2205            fitted_data.symbol = 'Line'
2206        # Notify the GUI manager so it can update the main model in DataExplorer
2207        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
2208
2209    def createTheoryIndex(self, fitted_data):
2210        """
2211        Create a QStandardModelIndex containing model data
2212        """
2213        name = self.nameFromData(fitted_data)
2214        # Notify the GUI manager so it can create the theory model in DataExplorer
2215        self.theory_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
2216        self.communicate.updateTheoryFromPerspectiveSignal.emit(self.theory_item)
2217
2218    def nameFromData(self, fitted_data):
2219        """
2220        Return name for the dataset. Terribly impure function.
2221        """
2222        if fitted_data.name is None:
2223            name = self.nameForFittedData(self.logic.data.filename)
2224            fitted_data.title = name
2225            fitted_data.name = name
2226            fitted_data.filename = name
2227        else:
2228            name = fitted_data.name
2229        return name
2230
2231    def methodCalculateForData(self):
2232        '''return the method for data calculation'''
2233        return Calc1D if isinstance(self.data, Data1D) else Calc2D
2234
2235    def methodCompleteForData(self):
2236        '''return the method for result parsin on calc complete '''
2237        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
2238
2239    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
2240        """
2241        Wrapper for Calc1D/2D calls
2242        """
2243        if data is None:
2244            data = self.data
2245        if model is None:
2246            model = self.kernel_module
2247        if completefn is None:
2248            completefn = self.methodCompleteForData()
2249        smearer = self.smearing_widget.smearer()
2250        # Awful API to a backend method.
2251        calc_thread = self.methodCalculateForData()(data=data,
2252                                               model=model,
2253                                               page_id=0,
2254                                               qmin=self.q_range_min,
2255                                               qmax=self.q_range_max,
2256                                               smearer=smearer,
2257                                               state=None,
2258                                               weight=None,
2259                                               fid=None,
2260                                               toggle_mode_on=False,
2261                                               completefn=completefn,
2262                                               update_chisqr=True,
2263                                               exception_handler=self.calcException,
2264                                               source=None)
2265        if use_threads:
2266            if LocalConfig.USING_TWISTED:
2267                # start the thread with twisted
2268                thread = threads.deferToThread(calc_thread.compute)
2269                thread.addCallback(completefn)
2270                thread.addErrback(self.calculateDataFailed)
2271            else:
2272                # Use the old python threads + Queue
2273                calc_thread.queue()
2274                calc_thread.ready(2.5)
2275        else:
2276            results = calc_thread.compute()
2277            completefn(results)
2278
2279    def calculateQGridForModel(self):
2280        """
2281        Prepare the fitting data object, based on current ModelModel
2282        """
2283        if self.kernel_module is None:
2284            return
2285        self.calculateQGridForModelExt()
2286
2287    def calculateDataFailed(self, reason):
2288        """
2289        Thread returned error
2290        """
2291        print("Calculate Data failed with ", reason)
2292
2293    def completed1D(self, return_data):
2294        self.Calc1DFinishedSignal.emit(return_data)
2295
2296    def completed2D(self, return_data):
2297        self.Calc2DFinishedSignal.emit(return_data)
2298
2299    def complete1D(self, return_data):
2300        """
2301        Plot the current 1D data
2302        """
2303        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
2304        residuals = self.calculateResiduals(fitted_data)
2305        self.model_data = fitted_data
2306        new_plots = [fitted_data]
2307        if residuals is not None:
2308            new_plots.append(residuals)
2309
2310        # Create plots for intermediate product data
2311        pq_data, sq_data = self.logic.new1DProductPlots(return_data, self.tab_id)
2312        if pq_data is not None:
2313            pq_data.symbol = "Line"
2314            self.createNewIndex(pq_data)
2315            # self.communicate.plotUpdateSignal.emit([pq_data])
2316            new_plots.append(pq_data)
2317        if sq_data is not None:
2318            sq_data.symbol = "Line"
2319            self.createNewIndex(sq_data)
2320            # self.communicate.plotUpdateSignal.emit([sq_data])
2321            new_plots.append(sq_data)
2322
2323        if self.data_is_loaded:
2324            GuiUtils.deleteRedundantPlots(self.all_data[self.data_index], new_plots)
2325
2326        # Update/generate plots
2327        for plot in new_plots:
2328            self.communicate.plotUpdateSignal.emit([plot])
2329
2330    def complete2D(self, return_data):
2331        """
2332        Plot the current 2D data
2333        """
2334        fitted_data = self.logic.new2DPlot(return_data)
2335        self.calculateResiduals(fitted_data)
2336        self.model_data = fitted_data
2337
2338    def calculateResiduals(self, fitted_data):
2339        """
2340        Calculate and print Chi2 and display chart of residuals. Returns residuals plot object.
2341        """
2342        # Create a new index for holding data
2343        fitted_data.symbol = "Line"
2344
2345        # Modify fitted_data with weighting
2346        self.addWeightingToData(fitted_data)
2347
2348        self.createNewIndex(fitted_data)
2349        # Calculate difference between return_data and logic.data
2350        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
2351        # Update the control
2352        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
2353        self.lblChi2Value.setText(chi2_repr)
2354
2355        # self.communicate.plotUpdateSignal.emit([fitted_data])
2356
2357        # Plot residuals if actual data
2358        if not self.data_is_loaded:
2359            return
2360
2361        residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
2362        residuals_plot.id = "Residual " + residuals_plot.id
2363        self.createNewIndex(residuals_plot)
2364        return residuals_plot
2365
2366    def onCategoriesChanged(self):
2367            """
2368            Reload the category/model comboboxes
2369            """
2370            # Store the current combo indices
2371            current_cat = self.cbCategory.currentText()
2372            current_model = self.cbModel.currentText()
2373
2374            # reread the category file and repopulate the combo
2375            self.cbCategory.blockSignals(True)
2376            self.cbCategory.clear()
2377            self.readCategoryInfo()
2378            self.initializeCategoryCombo()
2379
2380            # Scroll back to the original index in Categories
2381            new_index = self.cbCategory.findText(current_cat)
2382            if new_index != -1:
2383                self.cbCategory.setCurrentIndex(new_index)
2384            self.cbCategory.blockSignals(False)
2385            # ...and in the Models
2386            self.cbModel.blockSignals(True)
2387            new_index = self.cbModel.findText(current_model)
2388            if new_index != -1:
2389                self.cbModel.setCurrentIndex(new_index)
2390            self.cbModel.blockSignals(False)
2391
2392            return
2393
2394    def calcException(self, etype, value, tb):
2395        """
2396        Thread threw an exception.
2397        """
2398        # TODO: remimplement thread cancellation
2399        logging.error("".join(traceback.format_exception(etype, value, tb)))
2400
2401    def setTableProperties(self, table):
2402        """
2403        Setting table properties
2404        """
2405        # Table properties
2406        table.verticalHeader().setVisible(False)
2407        table.setAlternatingRowColors(True)
2408        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2409        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
2410        table.resizeColumnsToContents()
2411
2412        # Header
2413        header = table.horizontalHeader()
2414        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2415        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
2416
2417        # Qt5: the following 2 lines crash - figure out why!
2418        # Resize column 0 and 7 to content
2419        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2420        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
2421
2422    def setPolyModel(self):
2423        """
2424        Set polydispersity values
2425        """
2426        if not self.model_parameters:
2427            return
2428        self._poly_model.clear()
2429
2430        parameters = self.model_parameters.form_volume_parameters
2431        if self.is2D:
2432            parameters += self.model_parameters.orientation_parameters
2433
2434        [self.setPolyModelParameters(i, param) for i, param in \
2435            enumerate(parameters) if param.polydisperse]
2436
2437        FittingUtilities.addPolyHeadersToModel(self._poly_model)
2438
2439    def setPolyModelParameters(self, i, param):
2440        """
2441        Standard of multishell poly parameter driver
2442        """
2443        param_name = param.name
2444        # see it the parameter is multishell
2445        if '[' in param.name:
2446            # Skip empty shells
2447            if self.current_shell_displayed == 0:
2448                return
2449            else:
2450                # Create as many entries as current shells
2451                for ishell in range(1, self.current_shell_displayed+1):
2452                    # Remove [n] and add the shell numeral
2453                    name = param_name[0:param_name.index('[')] + str(ishell)
2454                    self.addNameToPolyModel(i, name)
2455        else:
2456            # Just create a simple param entry
2457            self.addNameToPolyModel(i, param_name)
2458
2459    def addNameToPolyModel(self, i, param_name):
2460        """
2461        Creates a checked row in the poly model with param_name
2462        """
2463        # Polydisp. values from the sasmodel
2464        width = self.kernel_module.getParam(param_name + '.width')
2465        npts = self.kernel_module.getParam(param_name + '.npts')
2466        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2467        _, min, max = self.kernel_module.details[param_name]
2468
2469        # Construct a row with polydisp. related variable.
2470        # This will get added to the polydisp. model
2471        # Note: last argument needs extra space padding for decent display of the control
2472        checked_list = ["Distribution of " + param_name, str(width),
2473                        str(min), str(max),
2474                        str(npts), str(nsigs), "gaussian      ",'']
2475        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2476
2477        # All possible polydisp. functions as strings in combobox
2478        func = QtWidgets.QComboBox()
2479        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
2480        # Set the default index
2481        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
2482        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2483        self.lstPoly.setIndexWidget(ind, func)
2484        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2485
2486    def onPolyFilenameChange(self, row_index):
2487        """
2488        Respond to filename_updated signal from the delegate
2489        """
2490        # For the given row, invoke the "array" combo handler
2491        array_caption = 'array'
2492
2493        # Get the combo box reference
2494        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2495        widget = self.lstPoly.indexWidget(ind)
2496
2497        # Update the combo box so it displays "array"
2498        widget.blockSignals(True)
2499        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
2500        widget.blockSignals(False)
2501
2502        # Invoke the file reader
2503        self.onPolyComboIndexChange(array_caption, row_index)
2504
2505    def onPolyComboIndexChange(self, combo_string, row_index):
2506        """
2507        Modify polydisp. defaults on function choice
2508        """
2509        # Get npts/nsigs for current selection
2510        param = self.model_parameters.form_volume_parameters[row_index]
2511        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2512        combo_box = self.lstPoly.indexWidget(file_index)
2513
2514        def updateFunctionCaption(row):
2515            # Utility function for update of polydispersity function name in the main model
2516            param_name = str(self._model_model.item(row, 0).text())
2517            if param_name !=  param.name:
2518                return
2519            # Modify the param value
2520            if self.has_error_column:
2521                # err column changes the indexing
2522                self._model_model.item(row, 0).child(0).child(0,5).setText(combo_string)
2523            else:
2524                self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
2525
2526        if combo_string == 'array':
2527            try:
2528                self.loadPolydispArray(row_index)
2529                # Update main model for display
2530                self.iterateOverModel(updateFunctionCaption)
2531                # disable the row
2532                lo = self.lstPoly.itemDelegate().poly_pd
2533                hi = self.lstPoly.itemDelegate().poly_function
2534                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
2535                return
2536            except IOError:
2537                combo_box.setCurrentIndex(self.orig_poly_index)
2538                # Pass for cancel/bad read
2539                pass
2540
2541        # Enable the row in case it was disabled by Array
2542        self._poly_model.blockSignals(True)
2543        max_range = self.lstPoly.itemDelegate().poly_filename
2544        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
2545        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2546        self._poly_model.setData(file_index, "")
2547        self._poly_model.blockSignals(False)
2548
2549        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
2550        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
2551
2552        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
2553        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
2554
2555        self._poly_model.setData(npts_index, npts)
2556        self._poly_model.setData(nsigs_index, nsigs)
2557
2558        self.iterateOverModel(updateFunctionCaption)
2559        self.orig_poly_index = combo_box.currentIndex()
2560
2561    def loadPolydispArray(self, row_index):
2562        """
2563        Show the load file dialog and loads requested data into state
2564        """
2565        datafile = QtWidgets.QFileDialog.getOpenFileName(
2566            self, "Choose a weight file", "", "All files (*.*)", None,
2567            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
2568
2569        if not datafile:
2570            logging.info("No weight data chosen.")
2571            raise IOError
2572
2573        values = []
2574        weights = []
2575        def appendData(data_tuple):
2576            """
2577            Fish out floats from a tuple of strings
2578            """
2579            try:
2580                values.append(float(data_tuple[0]))
2581                weights.append(float(data_tuple[1]))
2582            except (ValueError, IndexError):
2583                # just pass through if line with bad data
2584                return
2585
2586        with open(datafile, 'r') as column_file:
2587            column_data = [line.rstrip().split() for line in column_file.readlines()]
2588            [appendData(line) for line in column_data]
2589
2590        # If everything went well - update the sasmodel values
2591        self.disp_model = POLYDISPERSITY_MODELS['array']()
2592        self.disp_model.set_weights(np.array(values), np.array(weights))
2593        # + update the cell with filename
2594        fname = os.path.basename(str(datafile))
2595        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2596        self._poly_model.setData(fname_index, fname)
2597
2598    def setMagneticModel(self):
2599        """
2600        Set magnetism values on model
2601        """
2602        if not self.model_parameters:
2603            return
2604        self._magnet_model.clear()
2605        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
2606            self.model_parameters.call_parameters if param.type == 'magnetic']
2607        FittingUtilities.addHeadersToModel(self._magnet_model)
2608
2609    def shellNamesList(self):
2610        """
2611        Returns list of names of all multi-shell parameters
2612        E.g. for sld[n], radius[n], n=1..3 it will return
2613        [sld1, sld2, sld3, radius1, radius2, radius3]
2614        """
2615        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
2616        top_index = self.kernel_module.multiplicity_info.number
2617        shell_names = []
2618        for i in range(1, top_index+1):
2619            for name in multi_names:
2620                shell_names.append(name+str(i))
2621        return shell_names
2622
2623    def addCheckedMagneticListToModel(self, param, model):
2624        """
2625        Wrapper for model update with a subset of magnetic parameters
2626        """
2627        if param.name[param.name.index(':')+1:] in self.shell_names:
2628            # check if two-digit shell number
2629            try:
2630                shell_index = int(param.name[-2:])
2631            except ValueError:
2632                shell_index = int(param.name[-1:])
2633
2634            if shell_index > self.current_shell_displayed:
2635                return
2636
2637        checked_list = [param.name,
2638                        str(param.default),
2639                        str(param.limits[0]),
2640                        str(param.limits[1]),
2641                        param.units]
2642
2643        FittingUtilities.addCheckedListToModel(model, checked_list)
2644
2645    def enableStructureFactorControl(self, structure_factor):
2646        """
2647        Add structure factors to the list of parameters
2648        """
2649        if self.kernel_module.is_form_factor or structure_factor == 'None':
2650            self.enableStructureCombo()
2651        else:
2652            self.disableStructureCombo()
2653
2654    def addExtraShells(self):
2655        """
2656        Add a combobox for multiple shell display
2657        """
2658        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
2659
2660        if param_length == 0:
2661            return
2662
2663        # cell 1: variable name
2664        item1 = QtGui.QStandardItem(param_name)
2665
2666        func = QtWidgets.QComboBox()
2667        # Available range of shells displayed in the combobox
2668        func.addItems([str(i) for i in range(param_length+1)])
2669
2670        # Respond to index change
2671        func.currentIndexChanged.connect(self.modifyShellsInList)
2672
2673        # cell 2: combobox
2674        item2 = QtGui.QStandardItem()
2675        self._model_model.appendRow([item1, item2])
2676
2677        # Beautify the row:  span columns 2-4
2678        shell_row = self._model_model.rowCount()
2679        shell_index = self._model_model.index(shell_row-1, 1)
2680
2681        self.lstParams.setIndexWidget(shell_index, func)
2682        self._last_model_row = self._model_model.rowCount()
2683
2684        # Set the index to the state-kept value
2685        func.setCurrentIndex(self.current_shell_displayed
2686                             if self.current_shell_displayed < func.count() else 0)
2687
2688    def modifyShellsInList(self, index):
2689        """
2690        Add/remove additional multishell parameters
2691        """
2692        # Find row location of the combobox
2693        last_row = self._last_model_row
2694        remove_rows = self._model_model.rowCount() - last_row
2695
2696        if remove_rows > 1:
2697            self._model_model.removeRows(last_row, remove_rows)
2698
2699        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
2700        self.current_shell_displayed = index
2701
2702        # Update relevant models
2703        self.setPolyModel()
2704        self.setMagneticModel()
2705
2706    def setFittingStarted(self):
2707        """
2708        Set buttion caption on fitting start
2709        """
2710        # Notify the user that fitting is being run
2711        # Allow for stopping the job
2712        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
2713        self.cmdFit.setText('Stop fit')
2714
2715    def setFittingStopped(self):
2716        """
2717        Set button caption on fitting stop
2718        """
2719        # Notify the user that fitting is available
2720        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
2721        self.cmdFit.setText("Fit")
2722        self.fit_started = False
2723
2724    def readFitPage(self, fp):
2725        """
2726        Read in state from a fitpage object and update GUI
2727        """
2728        assert isinstance(fp, FitPage)
2729        # Main tab info
2730        self.logic.data.filename = fp.filename
2731        self.data_is_loaded = fp.data_is_loaded
2732        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
2733        self.chkMagnetism.setCheckState(fp.is_magnetic)
2734        self.chk2DView.setCheckState(fp.is2D)
2735
2736        # Update the comboboxes
2737        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
2738        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
2739        if fp.current_factor:
2740            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
2741
2742        self.chi2 = fp.chi2
2743
2744        # Options tab
2745        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
2746        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
2747        self.npts = fp.fit_options[fp.NPTS]
2748        self.log_points = fp.fit_options[fp.LOG_POINTS]
2749        self.weighting = fp.fit_options[fp.WEIGHTING]
2750
2751        # Models
2752        self._model_model = fp.model_model
2753        self._poly_model = fp.poly_model
2754        self._magnet_model = fp.magnetism_model
2755
2756        # Resolution tab
2757        smearing = fp.smearing_options[fp.SMEARING_OPTION]
2758        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
2759        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
2760        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
2761        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
2762
2763        # TODO: add polidyspersity and magnetism
2764
2765    def saveToFitPage(self, fp):
2766        """
2767        Write current state to the given fitpage
2768        """
2769        assert isinstance(fp, FitPage)
2770
2771        # Main tab info
2772        fp.filename = self.logic.data.filename
2773        fp.data_is_loaded = self.data_is_loaded
2774        fp.is_polydisperse = self.chkPolydispersity.isChecked()
2775        fp.is_magnetic = self.chkMagnetism.isChecked()
2776        fp.is2D = self.chk2DView.isChecked()
2777        fp.data = self.data
2778
2779        # Use current models - they contain all the required parameters
2780        fp.model_model = self._model_model
2781        fp.poly_model = self._poly_model
2782        fp.magnetism_model = self._magnet_model
2783
2784        if self.cbCategory.currentIndex() != 0:
2785            fp.current_category = str(self.cbCategory.currentText())
2786            fp.current_model = str(self.cbModel.currentText())
2787
2788        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
2789            fp.current_factor = str(self.cbStructureFactor.currentText())
2790        else:
2791            fp.current_factor = ''
2792
2793        fp.chi2 = self.chi2
2794        fp.main_params_to_fit = self.main_params_to_fit
2795        fp.poly_params_to_fit = self.poly_params_to_fit
2796        fp.magnet_params_to_fit = self.magnet_params_to_fit
2797        fp.kernel_module = self.kernel_module
2798
2799        # Algorithm options
2800        # fp.algorithm = self.parent.fit_options.selected_id
2801
2802        # Options tab
2803        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
2804        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
2805        fp.fit_options[fp.NPTS] = self.npts
2806        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
2807        fp.fit_options[fp.LOG_POINTS] = self.log_points
2808        fp.fit_options[fp.WEIGHTING] = self.weighting
2809
2810        # Resolution tab
2811        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
2812        fp.smearing_options[fp.SMEARING_OPTION] = smearing
2813        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
2814        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
2815        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
2816
2817        # TODO: add polidyspersity and magnetism
2818
2819
2820    def updateUndo(self):
2821        """
2822        Create a new state page and add it to the stack
2823        """
2824        if self.undo_supported:
2825            self.pushFitPage(self.currentState())
2826
2827    def currentState(self):
2828        """
2829        Return fit page with current state
2830        """
2831        new_page = FitPage()
2832        self.saveToFitPage(new_page)
2833
2834        return new_page
2835
2836    def pushFitPage(self, new_page):
2837        """
2838        Add a new fit page object with current state
2839        """
2840        self.page_stack.append(new_page)
2841
2842    def popFitPage(self):
2843        """
2844        Remove top fit page from stack
2845        """
2846        if self.page_stack:
2847            self.page_stack.pop()
2848
2849    def getReport(self):
2850        """
2851        Create and return HTML report with parameters and charts
2852        """
2853        index = None
2854        if self.all_data:
2855            index = self.all_data[self.data_index]
2856        else:
2857            index = self.theory_item
2858        report_logic = ReportPageLogic(self,
2859                                       kernel_module=self.kernel_module,
2860                                       data=self.data,
2861                                       index=index,
2862                                       model=self._model_model)
2863
2864        return report_logic.reportList()
2865
2866    def savePageState(self):
2867        """
2868        Create and serialize local PageState
2869        """
2870        from sas.sascalc.fit.pagestate import Reader
2871        model = self.kernel_module
2872
2873        # Old style PageState object
2874        state = PageState(model=model, data=self.data)
2875
2876        # Add parameter data to the state
2877        self.getCurrentFitState(state)
2878
2879        # Create the filewriter, aptly named 'Reader'
2880        state_reader = Reader(self.loadPageStateCallback)
2881        filepath = self.saveAsAnalysisFile()
2882        if filepath is None or filepath == "":
2883            return
2884        state_reader.write(filename=filepath, fitstate=state)
2885        pass
2886
2887    def saveAsAnalysisFile(self):
2888        """
2889        Show the save as... dialog and return the chosen filepath
2890        """
2891        default_name = "FitPage"+str(self.tab_id)+".fitv"
2892
2893        wildcard = "fitv files (*.fitv)"
2894        kwargs = {
2895            'caption'   : 'Save As',
2896            'directory' : default_name,
2897            'filter'    : wildcard,
2898            'parent'    : None,
2899        }
2900        # Query user for filename.
2901        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
2902        filename = filename_tuple[0]
2903        return filename
2904
2905    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
2906        """
2907        This is a callback method called from the CANSAS reader.
2908        We need the instance of this reader only for writing out a file,
2909        so there's nothing here.
2910        Until Load Analysis is implemented, that is.
2911        """
2912        pass
2913
2914    def loadPageState(self, pagestate=None):
2915        """
2916        Load the PageState object and update the current widget
2917        """
2918        pass
2919
2920    def getCurrentFitState(self, state=None):
2921        """
2922        Store current state for fit_page
2923        """
2924        # save model option
2925        #if self.model is not None:
2926        #    self.disp_list = self.getDispParamList()
2927        #    state.disp_list = copy.deepcopy(self.disp_list)
2928        #    #state.model = self.model.clone()
2929
2930        # Comboboxes
2931        state.categorycombobox = self.cbCategory.currentText()
2932        state.formfactorcombobox = self.cbModel.currentText()
2933        if self.cbStructureFactor.isEnabled():
2934            state.structurecombobox = self.cbStructureFactor.currentText()
2935        state.tcChi = self.chi2
2936
2937        state.enable2D = self.is2D
2938
2939        #state.weights = copy.deepcopy(self.weights)
2940        # save data
2941        state.data = copy.deepcopy(self.data)
2942
2943        # save plotting range
2944        state.qmin = self.q_range_min
2945        state.qmax = self.q_range_max
2946        state.npts = self.npts
2947
2948        #    self.state.enable_disp = self.enable_disp.GetValue()
2949        #    self.state.disable_disp = self.disable_disp.GetValue()
2950
2951        #    self.state.enable_smearer = \
2952        #                        copy.deepcopy(self.enable_smearer.GetValue())
2953        #    self.state.disable_smearer = \
2954        #                        copy.deepcopy(self.disable_smearer.GetValue())
2955
2956        #self.state.pinhole_smearer = \
2957        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
2958        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
2959        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
2960        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
2961        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
2962        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
2963
2964        p = self.model_parameters
2965        # save checkbutton state and txtcrtl values
2966        state.parameters = FittingUtilities.getStandardParam(self._model_model)
2967        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
2968
2969        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
2970        #self._copy_parameters_state(self.parameters, self.state.parameters)
2971        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
2972        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
2973
2974    def onParameterCopy(self, format=None):
2975        """
2976        Copy current parameters into the clipboard
2977        """
2978        # run a loop over all parameters and pull out
2979        # first - regular params
2980        param_list = []
2981        def gatherParams(row):
2982            """
2983            Create list of main parameters based on _model_model
2984            """
2985            param_name = str(self._model_model.item(row, 0).text())
2986            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
2987            param_value = str(self._model_model.item(row, 1).text())
2988            param_error = None
2989            column_offset = 0
2990            if self.has_error_column:
2991                param_error = str(self._model_model.item(row, 2).text())
2992                column_offset = 1
2993            param_min = str(self._model_model.item(row, 2+column_offset).text())
2994            param_max = str(self._model_model.item(row, 3+column_offset).text())
2995            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
2996
2997        def gatherPolyParams(row):
2998            """
2999            Create list of polydisperse parameters based on _poly_model
3000            """
3001            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
3002            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3003            param_value = str(self._poly_model.item(row, 1).text())
3004            param_error = None
3005            column_offset = 0
3006            if self.has_poly_error_column:
3007                param_error = str(self._poly_model.item(row, 2).text())
3008                column_offset = 1
3009            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
3010            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
3011            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
3012            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
3013            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
3014            # width
3015            name = param_name+".width"
3016            param_list.append([name, param_checked, param_value, param_error,
3017                                param_npts, param_nsigs, param_min, param_max, param_fun])
3018
3019        def gatherMagnetParams(row):
3020            """
3021            Create list of magnetic parameters based on _magnet_model
3022            """
3023            param_name = str(self._magnet_model.item(row, 0).text())
3024            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3025            param_value = str(self._magnet_model.item(row, 1).text())
3026            param_error = None
3027            column_offset = 0
3028            if self.has_magnet_error_column:
3029                param_error = str(self._magnet_model.item(row, 2).text())
3030                column_offset = 1
3031            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
3032            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
3033            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3034
3035        self.iterateOverModel(gatherParams)
3036        if self.chkPolydispersity.isChecked():
3037            self.iterateOverPolyModel(gatherPolyParams)
3038        if self.chkMagnetism.isChecked() and self.chkMagnetism.isEnabled():
3039            self.iterateOverMagnetModel(gatherMagnetParams)
3040
3041        if format=="":
3042            formatted_output = FittingUtilities.formatParameters(param_list)
3043        elif format == "Excel":
3044            formatted_output = FittingUtilities.formatParametersExcel(param_list)
3045        elif format == "Latex":
3046            formatted_output = FittingUtilities.formatParametersLatex(param_list)
3047        else:
3048            raise AttributeError("Bad format specifier.")
3049
3050        # Dump formatted_output to the clipboard
3051        cb = QtWidgets.QApplication.clipboard()
3052        cb.setText(formatted_output)
3053
3054    def onParameterPaste(self):
3055        """
3056        Use the clipboard to update fit state
3057        """
3058        # Check if the clipboard contains right stuff
3059        cb = QtWidgets.QApplication.clipboard()
3060        cb_text = cb.text()
3061
3062        context = {}
3063        # put the text into dictionary
3064        lines = cb_text.split(':')
3065        if lines[0] != 'sasview_parameter_values':
3066            return False
3067        for line in lines[1:-1]:
3068            if len(line) != 0:
3069                item = line.split(',')
3070                check = item[1]
3071                name = item[0]
3072                value = item[2]
3073                # Transfer the text to content[dictionary]
3074                context[name] = [check, value]
3075
3076                # limits
3077                limit_lo = item[3]
3078                context[name].append(limit_lo)
3079                limit_hi = item[4]
3080                context[name].append(limit_hi)
3081
3082                # Polydisp
3083                if len(item) > 5:
3084                    value = item[5]
3085                    context[name].append(value)
3086                    try:
3087                        value = item[6]
3088                        context[name].append(value)
3089                        value = item[7]
3090                        context[name].append(value)
3091                    except IndexError:
3092                        pass
3093
3094        self.updateFullModel(context)
3095        self.updateFullPolyModel(context)
3096
3097    def updateFullModel(self, param_dict):
3098        """
3099        Update the model with new parameters
3100        """
3101        assert isinstance(param_dict, dict)
3102        if not dict:
3103            return
3104
3105        def updateFittedValues(row):
3106            # Utility function for main model update
3107            # internal so can use closure for param_dict
3108            param_name = str(self._model_model.item(row, 0).text())
3109            if param_name not in list(param_dict.keys()):
3110                return
3111            # checkbox state
3112            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3113            self._model_model.item(row, 0).setCheckState(param_checked)
3114
3115            # modify the param value
3116            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3117            self._model_model.item(row, 1).setText(param_repr)
3118
3119            # Potentially the error column
3120            ioffset = 0
3121            if len(param_dict[param_name])>4 and self.has_error_column:
3122                # error values are not editable - no need to update
3123                #error_repr = GuiUtils.formatNumber(param_dict[param_name][2], high=True)
3124                #self._model_model.item(row, 2).setText(error_repr)
3125                ioffset = 1
3126            # min/max
3127            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3128            self._model_model.item(row, 2+ioffset).setText(param_repr)
3129            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3130            self._model_model.item(row, 3+ioffset).setText(param_repr)
3131
3132        # block signals temporarily, so we don't end up
3133        # updating charts with every single model change on the end of fitting
3134        self._model_model.blockSignals(True)
3135        self.iterateOverModel(updateFittedValues)
3136        self._model_model.blockSignals(False)
3137
3138    def updateFullPolyModel(self, param_dict):
3139        """
3140        Update the polydispersity model with new parameters, create the errors column
3141        """
3142        assert isinstance(param_dict, dict)
3143        if not dict:
3144            return
3145
3146        def updateFittedValues(row):
3147            # Utility function for main model update
3148            # internal so can use closure for param_dict
3149            if row >= self._poly_model.rowCount():
3150                return
3151            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
3152            if param_name not in list(param_dict.keys()):
3153                return
3154            # checkbox state
3155            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3156            self._poly_model.item(row,0).setCheckState(param_checked)
3157
3158            # modify the param value
3159            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3160            self._poly_model.item(row, 1).setText(param_repr)
3161
3162            # Potentially the error column
3163            ioffset = 0
3164            if len(param_dict[param_name])>4 and self.has_poly_error_column:
3165                ioffset = 1
3166            # min
3167            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3168            self._poly_model.item(row, 2+ioffset).setText(param_repr)
3169            # max
3170            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3171            self._poly_model.item(row, 3+ioffset).setText(param_repr)
3172            # Npts
3173            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
3174            self._poly_model.item(row, 4+ioffset).setText(param_repr)
3175            # Nsigs
3176            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3177            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3178
3179            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3180            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3181
3182        # block signals temporarily, so we don't end up
3183        # updating charts with every single model change on the end of fitting
3184        self._poly_model.blockSignals(True)
3185        self.iterateOverPolyModel(updateFittedValues)
3186        self._poly_model.blockSignals(False)
3187
3188
Note: See TracBrowser for help on using the repository browser.