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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 8748751 was 8748751, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Fit result viewer SASVIEW-274, SASVIEW-275

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