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

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 605d944 was 605d944, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Fixed unit tests for fitting SASVIEW-1024
Corrected behaviour of the Structure factor combo box on model/category change

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