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

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 b764ae5 was b764ae5, checked in by Piotr Rozyczko <rozyczko@…>, 18 months ago

processEvents() helps with proper chart generation. - SASVIEW-890
Fixed weighing in fitting - SASVIEW-1017
Fixed error bars after fitting - SASVIEW-1004

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