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

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 04f775d was 04f775d, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

cherry-pick fixed-choice param support, made more generic and cleaner

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