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

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

residuals plot is None for theory-only tabs

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