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

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 0eff615 was 0eff615, checked in by Laura Forster <Awork@…>, 6 years ago

Paste params edited

When GUI loaded Paste is now disabled until user copies params. Paste also checks if model has changed since last copy, and displays message warning user that not all params may
paste, with option to cancel. Also - focus fixed such that if paste is clicked the params automatically update without having to move mouse to reset the focus.

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