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

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

More fixes from PK's CR

  • 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        params = FittingUtilities.getStandardParam(self._model_model)
3367        report_logic = ReportPageLogic(self,
3368                                       kernel_module=self.kernel_module,
3369                                       data=self.data,
3370                                       index=index,
3371                                       params=params)
3372
3373        return report_logic.reportList()
3374
3375    def savePageState(self):
3376        """
3377        Create and serialize local PageState
3378        """
3379        filepath = self.saveAsAnalysisFile()
3380        if filepath is None or filepath == "":
3381            return
3382
3383        fitpage_state = self.getFitPage()
3384        fitpage_state += self.getFitModel()
3385
3386        with open(filepath, 'w') as statefile:
3387            for line in fitpage_state:
3388                statefile.write(str(line))
3389
3390        self.communicate.statusBarUpdateSignal.emit('Analysis saved.')
3391
3392    def saveAsAnalysisFile(self):
3393        """
3394        Show the save as... dialog and return the chosen filepath
3395        """
3396        default_name = "FitPage"+str(self.tab_id)+".fitv"
3397
3398        wildcard = "fitv files (*.fitv)"
3399        kwargs = {
3400            'caption'   : 'Save As',
3401            'directory' : default_name,
3402            'filter'    : wildcard,
3403            'parent'    : None,
3404        }
3405        # Query user for filename.
3406        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
3407        filename = filename_tuple[0]
3408        return filename
3409
3410    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
3411        """
3412        This is a callback method called from the CANSAS reader.
3413        We need the instance of this reader only for writing out a file,
3414        so there's nothing here.
3415        Until Load Analysis is implemented, that is.
3416        """
3417        pass
3418
3419    def loadPageState(self, pagestate=None):
3420        """
3421        Load the PageState object and update the current widget
3422        """
3423        filepath = self.loadAnalysisFile()
3424        if filepath is None or filepath == "":
3425            return
3426
3427        with open(filepath, 'r') as statefile:
3428            #column_data = [line.rstrip().split() for line in statefile.readlines()]
3429            lines = statefile.readlines()
3430
3431        # convert into list of lists
3432        pass
3433
3434    def loadAnalysisFile(self):
3435        """
3436        Called when the "Open Project" menu item chosen.
3437        """
3438        default_name = "FitPage"+str(self.tab_id)+".fitv"
3439        wildcard = "fitv files (*.fitv)"
3440        kwargs = {
3441            'caption'   : 'Open Analysis',
3442            'directory' : default_name,
3443            'filter'    : wildcard,
3444            'parent'    : self,
3445        }
3446        filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
3447        return filename
3448
3449    def onCopyToClipboard(self, format=None):
3450        """
3451        Copy current fitting parameters into the clipboard
3452        using requested formatting:
3453        plain, excel, latex
3454        """
3455        param_list = self.getFitParameters()
3456        if format=="":
3457            param_list = self.getFitPage()
3458            param_list += self.getFitModel()
3459            formatted_output = FittingUtilities.formatParameters(param_list)
3460        elif format == "Excel":
3461            formatted_output = FittingUtilities.formatParametersExcel(param_list[1:])
3462        elif format == "Latex":
3463            formatted_output = FittingUtilities.formatParametersLatex(param_list[1:])
3464        else:
3465            raise AttributeError("Bad parameter output format specifier.")
3466
3467        # Dump formatted_output to the clipboard
3468        cb = QtWidgets.QApplication.clipboard()
3469        cb.setText(formatted_output)
3470
3471    def getFitModel(self):
3472        """
3473        serializes combobox state
3474        """
3475        param_list = []
3476        model = str(self.cbModel.currentText())
3477        category = str(self.cbCategory.currentText())
3478        structure = str(self.cbStructureFactor.currentText())
3479        param_list.append(['fitpage_category', category])
3480        param_list.append(['fitpage_model', model])
3481        param_list.append(['fitpage_structure', structure])
3482
3483        return param_list
3484
3485    def getFitPage(self):
3486        """
3487        serializes full state of this fit page
3488        """
3489        # run a loop over all parameters and pull out
3490        # first - regular params
3491        param_list = self.getFitParameters()
3492
3493        param_list.append(['is_data', str(self.data_is_loaded)])
3494        if self.data_is_loaded:
3495            param_list.append(['data_id', str(self.logic.data.id)])
3496            param_list.append(['data_name', str(self.logic.data.filename)])
3497
3498        # option tab
3499        param_list.append(['q_range_min', str(self.q_range_min)])
3500        param_list.append(['q_range_max', str(self.q_range_max)])
3501        param_list.append(['q_weighting', str(self.weighting)])
3502        param_list.append(['weighting', str(self.options_widget.weighting)])
3503
3504        # resolution
3505        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3506        index = self.smearing_widget.cbSmearing.currentIndex()
3507        param_list.append(['smearing', str(index)])
3508        param_list.append(['smearing_min', str(smearing_min)])
3509        param_list.append(['smearing_max', str(smearing_max)])
3510
3511        # checkboxes, if required
3512        has_polydisp = self.chkPolydispersity.isChecked()
3513        has_magnetism = self.chkMagnetism.isChecked()
3514        has_chain = self.chkChainFit.isChecked()
3515        has_2D = self.chk2DView.isChecked()
3516        param_list.append(['polydisperse_params', str(has_polydisp)])
3517        param_list.append(['magnetic_params', str(has_magnetism)])
3518        param_list.append(['chainfit_params', str(has_chain)])
3519        param_list.append(['2D_params', str(has_2D)])
3520
3521        return param_list
3522
3523    def getFitParameters(self):
3524        """
3525        serializes current parameters
3526        """
3527        param_list = []
3528        param_list.append(['model_name', str(self.cbModel.currentText())])
3529
3530        def gatherParams(row):
3531            """
3532            Create list of main parameters based on _model_model
3533            """
3534            param_name = str(self._model_model.item(row, 0).text())
3535
3536            # Assure this is a parameter - must contain a checkbox
3537            if not self._model_model.item(row, 0).isCheckable():
3538                # maybe it is a combobox item (multiplicity)
3539                try:
3540                    index = self._model_model.index(row, 1)
3541                    widget = self.lstParams.indexWidget(index)
3542                    if widget is None:
3543                        return
3544                    if isinstance(widget, QtWidgets.QComboBox):
3545                        # find the index of the combobox
3546                        current_index = widget.currentIndex()
3547                        param_list.append([param_name, 'None', str(current_index)])
3548                except Exception as ex:
3549                    pass
3550                return
3551
3552            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3553            # Value of the parameter. In some cases this is the text of the combobox choice.
3554            param_value = str(self._model_model.item(row, 1).text())
3555            param_error = None
3556            param_min = None
3557            param_max = None
3558            column_offset = 0
3559            if self.has_error_column:
3560                column_offset = 1
3561                param_error = str(self._model_model.item(row, 1+column_offset).text())
3562            try:
3563                param_min = str(self._model_model.item(row, 2+column_offset).text())
3564                param_max = str(self._model_model.item(row, 3+column_offset).text())
3565            except:
3566                pass
3567
3568            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3569
3570        def gatherPolyParams(row):
3571            """
3572            Create list of polydisperse parameters based on _poly_model
3573            """
3574            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
3575            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3576            param_value = str(self._poly_model.item(row, 1).text())
3577            param_error = None
3578            column_offset = 0
3579            if self.has_poly_error_column:
3580                column_offset = 1
3581                param_error = str(self._poly_model.item(row, 1+column_offset).text())
3582            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
3583            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
3584            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
3585            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
3586            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
3587            index = self._poly_model.index(row, 6+column_offset)
3588            widget = self.lstPoly.indexWidget(index)
3589            if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3590                param_fun = widget.currentText()
3591            # width
3592            name = param_name+".width"
3593            param_list.append([name, param_checked, param_value, param_error,
3594                               param_min, param_max, param_npts, param_nsigs, param_fun])
3595
3596        def gatherMagnetParams(row):
3597            """
3598            Create list of magnetic parameters based on _magnet_model
3599            """
3600            param_name = str(self._magnet_model.item(row, 0).text())
3601            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3602            param_value = str(self._magnet_model.item(row, 1).text())
3603            param_error = None
3604            column_offset = 0
3605            if self.has_magnet_error_column:
3606                column_offset = 1
3607                param_error = str(self._magnet_model.item(row, 1+column_offset).text())
3608            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
3609            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
3610            param_list.append([param_name, param_checked, param_value,
3611                               param_error, param_min, param_max])
3612
3613        self.iterateOverModel(gatherParams)
3614        if self.chkPolydispersity.isChecked():
3615            self.iterateOverPolyModel(gatherPolyParams)
3616        if self.chkMagnetism.isChecked() and self.canHaveMagnetism():
3617            self.iterateOverMagnetModel(gatherMagnetParams)
3618
3619        if self.kernel_module.is_multiplicity_model:
3620            param_list.append(['multiplicity', str(self.kernel_module.multiplicity)])
3621
3622        return param_list
3623
3624    def onParameterPaste(self):
3625        """
3626        Use the clipboard to update fit state
3627        """
3628        # Check if the clipboard contains right stuff
3629        cb = QtWidgets.QApplication.clipboard()
3630        cb_text = cb.text()
3631
3632        context = {}
3633        lines = cb_text.split(':')
3634        if lines[0] != 'sasview_parameter_values':
3635            return False
3636
3637        # put the text into dictionary
3638        line_dict = {}
3639        for line in lines[1:]:
3640            content = line.split(',')
3641            if len(content) > 1:
3642                line_dict[content[0]] = content[1:]
3643
3644        model = line_dict['model_name'][0]
3645
3646        if 'model_name' not in line_dict.keys():
3647            return False
3648
3649        if 'multiplicity' in line_dict.keys():
3650            multip = int(line_dict['multiplicity'][0], 0)
3651            # reset the model with multiplicity, so further updates are saved
3652            if self.kernel_module.is_multiplicity_model:
3653                self.kernel_module.multiplicity=multip
3654                self.updateMultiplicityCombo(multip)
3655
3656        if 'polydisperse_params' in line_dict.keys():
3657            self.chkPolydispersity.setChecked(line_dict['polydisperse_params'][0]=='True')
3658        if 'magnetic_params' in line_dict.keys():
3659            self.chkMagnetism.setChecked(line_dict['magnetic_params'][0]=='True')
3660        if 'chainfit_params' in line_dict.keys():
3661            self.chkChainFit.setChecked(line_dict['chainfit_params'][0]=='True')
3662        if '2D_params' in line_dict.keys():
3663            self.chk2DView.setChecked(line_dict['2D_params'][0]=='True')
3664
3665        # Create the context dictionary for parameters
3666        context['model_name'] = model
3667        for key, value in line_dict.items():
3668            if len(value) > 2:
3669                context[key] = value
3670
3671        if str(self.cbModel.currentText()) != str(context['model_name']):
3672            msg = QtWidgets.QMessageBox()
3673            msg.setIcon(QtWidgets.QMessageBox.Information)
3674            msg.setText("The model in the clipboard is not the same as the currently loaded model. \
3675                         Not all parameters saved may paste correctly.")
3676            msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
3677            result = msg.exec_()
3678            if result == QtWidgets.QMessageBox.Ok:
3679                pass
3680            else:
3681                return
3682
3683        if 'smearing' in line_dict.keys():
3684            try:
3685                index = int(line_dict['smearing'][0])
3686                self.smearing_widget.cbSmearing.setCurrentIndex(index)
3687            except ValueError:
3688                pass
3689        if 'smearing_min' in line_dict.keys():
3690            try:
3691                self.smearing_widget.dq_l = float(line_dict['smearing_min'][0])
3692            except ValueError:
3693                pass
3694        if 'smearing_max' in line_dict.keys():
3695            try:
3696                self.smearing_widget.dq_r = float(line_dict['smearing_max'][0])
3697            except ValueError:
3698                pass
3699
3700        if 'q_range_max' in line_dict.keys():
3701            try:
3702                self.q_range_min = float(line_dict['q_range_min'][0])
3703                self.q_range_max = float(line_dict['q_range_max'][0])
3704            except ValueError:
3705                pass
3706        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
3707        try:
3708            button_id = int(line_dict['weighting'][0])
3709            for button in self.options_widget.weightingGroup.buttons():
3710                if abs(self.options_widget.weightingGroup.id(button)) == button_id+2:
3711                    button.setChecked(True)
3712                    break
3713        except ValueError:
3714            pass
3715
3716        self.updateFullModel(context)
3717        self.updateFullPolyModel(context)
3718        self.updateFullMagnetModel(context)
3719
3720    def updateMultiplicityCombo(self, multip):
3721        """
3722        Find and update the multiplicity combobox
3723        """
3724        index = self._model_model.index(self._n_shells_row, 1)
3725        widget = self.lstParams.indexWidget(index)
3726        if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3727            widget.setCurrentIndex(widget.findText(str(multip)))
3728        self.current_shell_displayed = multip
3729
3730    def updateFullModel(self, param_dict):
3731        """
3732        Update the model with new parameters
3733        """
3734        assert isinstance(param_dict, dict)
3735        if not dict:
3736            return
3737
3738        def updateFittedValues(row):
3739            # Utility function for main model update
3740            # internal so can use closure for param_dict
3741            param_name = str(self._model_model.item(row, 0).text())
3742            if param_name not in list(param_dict.keys()):
3743                return
3744            # Special case of combo box in the cell (multiplicity)
3745            param_line = param_dict[param_name]
3746            if len(param_line) == 1:
3747                # modify the shells value
3748                try:
3749                    combo_index = int(param_line[0])
3750                except ValueError:
3751                    # quietly pass
3752                    return
3753                index = self._model_model.index(row, 1)
3754                widget = self.lstParams.indexWidget(index)
3755                if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3756                    #widget.setCurrentIndex(combo_index)
3757                    return
3758            # checkbox state
3759            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3760            self._model_model.item(row, 0).setCheckState(param_checked)
3761
3762            # parameter value can be either just a value or text on the combobox
3763            param_text = param_dict[param_name][1]
3764            index = self._model_model.index(row, 1)
3765            widget = self.lstParams.indexWidget(index)
3766            if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3767                # Find the right index based on text
3768                combo_index = int(param_text, 0)
3769                widget.setCurrentIndex(combo_index)
3770            else:
3771                # modify the param value
3772                param_repr = GuiUtils.formatNumber(param_text, high=True)
3773                self._model_model.item(row, 1).setText(param_repr)
3774
3775            # Potentially the error column
3776            ioffset = 0
3777            joffset = 0
3778            if len(param_dict[param_name])>4:
3779                # error values are not editable - no need to update
3780                ioffset = 1
3781            if self.has_error_column:
3782                joffset = 1
3783            # min/max
3784            try:
3785                param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3786                self._model_model.item(row, 2+joffset).setText(param_repr)
3787                param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3788                self._model_model.item(row, 3+joffset).setText(param_repr)
3789            except:
3790                pass
3791
3792            self.setFocus()
3793
3794        self.iterateOverModel(updateFittedValues)
3795
3796    def updateFullPolyModel(self, param_dict):
3797        """
3798        Update the polydispersity model with new parameters, create the errors column
3799        """
3800        assert isinstance(param_dict, dict)
3801        if not dict:
3802            return
3803
3804        def updateFittedValues(row):
3805            # Utility function for main model update
3806            # internal so can use closure for param_dict
3807            if row >= self._poly_model.rowCount():
3808                return
3809            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
3810            if param_name not in list(param_dict.keys()):
3811                return
3812            # checkbox state
3813            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3814            self._poly_model.item(row,0).setCheckState(param_checked)
3815
3816            # modify the param value
3817            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3818            self._poly_model.item(row, 1).setText(param_repr)
3819
3820            # Potentially the error column
3821            ioffset = 0
3822            joffset = 0
3823            if len(param_dict[param_name])>7:
3824                ioffset = 1
3825            if self.has_poly_error_column:
3826                joffset = 1
3827            # min
3828            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3829            self._poly_model.item(row, 2+joffset).setText(param_repr)
3830            # max
3831            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3832            self._poly_model.item(row, 3+joffset).setText(param_repr)
3833            # Npts
3834            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
3835            self._poly_model.item(row, 4+joffset).setText(param_repr)
3836            # Nsigs
3837            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3838            self._poly_model.item(row, 5+joffset).setText(param_repr)
3839
3840            self.setFocus()
3841
3842        self.iterateOverPolyModel(updateFittedValues)
3843
3844    def updateFullMagnetModel(self, param_dict):
3845        """
3846        Update the magnetism model with new parameters, create the errors column
3847        """
3848        assert isinstance(param_dict, dict)
3849        if not dict:
3850            return
3851
3852        def updateFittedValues(row):
3853            # Utility function for main model update
3854            # internal so can use closure for param_dict
3855            if row >= self._magnet_model.rowCount():
3856                return
3857            param_name = str(self._magnet_model.item(row, 0).text()).rsplit()[-1]
3858            if param_name not in list(param_dict.keys()):
3859                return
3860            # checkbox state
3861            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3862            self._magnet_model.item(row,0).setCheckState(param_checked)
3863
3864            # modify the param value
3865            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3866            self._magnet_model.item(row, 1).setText(param_repr)
3867
3868            # Potentially the error column
3869            ioffset = 0
3870            joffset = 0
3871            if len(param_dict[param_name])>4:
3872                ioffset = 1
3873            if self.has_magnet_error_column:
3874                joffset = 1
3875            # min
3876            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3877            self._magnet_model.item(row, 2+joffset).setText(param_repr)
3878            # max
3879            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3880            self._magnet_model.item(row, 3+joffset).setText(param_repr)
3881
3882        self.iterateOverMagnetModel(updateFittedValues)
3883
3884    def getCurrentFitState(self, state=None):
3885        """
3886        Store current state for fit_page
3887        """
3888        # save model option
3889        #if self.model is not None:
3890        #    self.disp_list = self.getDispParamList()
3891        #    state.disp_list = copy.deepcopy(self.disp_list)
3892        #    #state.model = self.model.clone()
3893
3894        # Comboboxes
3895        state.categorycombobox = self.cbCategory.currentText()
3896        state.formfactorcombobox = self.cbModel.currentText()
3897        if self.cbStructureFactor.isEnabled():
3898            state.structurecombobox = self.cbStructureFactor.currentText()
3899        state.tcChi = self.chi2
3900
3901        state.enable2D = self.is2D
3902
3903        #state.weights = copy.deepcopy(self.weights)
3904        # save data
3905        state.data = copy.deepcopy(self.data)
3906
3907        # save plotting range
3908        state.qmin = self.q_range_min
3909        state.qmax = self.q_range_max
3910        state.npts = self.npts
3911
3912        #    self.state.enable_disp = self.enable_disp.GetValue()
3913        #    self.state.disable_disp = self.disable_disp.GetValue()
3914
3915        #    self.state.enable_smearer = \
3916        #                        copy.deepcopy(self.enable_smearer.GetValue())
3917        #    self.state.disable_smearer = \
3918        #                        copy.deepcopy(self.disable_smearer.GetValue())
3919
3920        #self.state.pinhole_smearer = \
3921        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
3922        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
3923        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
3924        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
3925        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
3926        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
3927
3928        p = self.model_parameters
3929        # save checkbutton state and txtcrtl values
3930        state.parameters = FittingUtilities.getStandardParam(self._model_model)
3931        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
3932
3933        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
3934        #self._copy_parameters_state(self.parameters, self.state.parameters)
3935        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
3936        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
3937
Note: See TracBrowser for help on using the repository browser.