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

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

Mask Edit menu item added to Fitting

Mask edit was previously only available via right clicking data before fit, now available on fitting drop down menu option.

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