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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 1e0296b was 1e0296b, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 22 months ago

fitpage copy/paste fix for error column counter

(cherry picked from commit b8dccb8c597afe425c67825bd81c0247a9faa1e3)

  • Property mode set to 100644
File size: 150.5 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
1561        elapsed = result[1]
1562        if self.calc_fit is not None and self.calc_fit._interrupting:
1563            msg = "Fitting cancelled by user after: %s s." % GuiUtils.formatNumber(elapsed)
1564            logger.warning("\n"+msg+"\n")
1565        else:
1566            msg = "Fitting completed successfully in: %s s." % GuiUtils.formatNumber(elapsed)
1567        self.communicate.statusBarUpdateSignal.emit(msg)
1568
1569        # Dictionary of fitted parameter: value, error
1570        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
1571        self.updateModelFromList(param_dict)
1572
1573        self.updatePolyModelFromList(param_dict)
1574
1575        self.updateMagnetModelFromList(param_dict)
1576
1577        # update charts
1578        self.onPlot()
1579        #self.recalculatePlotData()
1580
1581
1582        # Read only value - we can get away by just printing it here
1583        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
1584        self.lblChi2Value.setText(chi2_repr)
1585
1586    def prepareFitters(self, fitter=None, fit_id=0):
1587        """
1588        Prepare the Fitter object for use in fitting
1589        """
1590        # fitter = None -> single/batch fitting
1591        # fitter = Fit() -> simultaneous fitting
1592
1593        # Data going in
1594        data = self.logic.data
1595        model = copy.deepcopy(self.kernel_module)
1596        qmin = self.q_range_min
1597        qmax = self.q_range_max
1598        # add polydisperse/magnet parameters if asked
1599        self.updateKernelModelWithExtraParams(model)
1600
1601        params_to_fit = copy.deepcopy(self.main_params_to_fit)
1602        if self.chkPolydispersity.isChecked():
1603            params_to_fit += self.poly_params_to_fit
1604        if self.chkMagnetism.isChecked() and self.canHaveMagnetism():
1605            params_to_fit += self.magnet_params_to_fit
1606        if not params_to_fit:
1607            raise ValueError('Fitting requires at least one parameter to optimize.')
1608
1609        # Get the constraints.
1610        constraints = self.getComplexConstraintsForModel()
1611        if fitter is None:
1612            # For single fits - check for inter-model constraints
1613            constraints = self.getConstraintsForFitting()
1614
1615        smearer = self.smearing_widget.smearer()
1616        handler = None
1617        batch_inputs = {}
1618        batch_outputs = {}
1619
1620        fitters = []
1621        for fit_index in self.all_data:
1622            fitter_single = Fit() if fitter is None else fitter
1623            data = GuiUtils.dataFromItem(fit_index)
1624            # Potential weights added directly to data
1625            weighted_data = self.addWeightingToData(data)
1626            try:
1627                fitter_single.set_model(model, fit_id, params_to_fit, data=weighted_data,
1628                             constraints=constraints)
1629            except ValueError as ex:
1630                raise ValueError("Setting model parameters failed with: %s" % ex)
1631
1632            qmin, qmax, _ = self.logic.computeRangeFromData(weighted_data)
1633            fitter_single.set_data(data=weighted_data, id=fit_id, smearer=smearer, qmin=qmin,
1634                            qmax=qmax)
1635            fitter_single.select_problem_for_fit(id=fit_id, value=1)
1636            if fitter is None:
1637                # Assign id to the new fitter only
1638                fitter_single.fitter_id = [self.page_id]
1639            fit_id += 1
1640            fitters.append(fitter_single)
1641
1642        return fitters, fit_id
1643
1644    def iterateOverModel(self, func):
1645        """
1646        Take func and throw it inside the model row loop
1647        """
1648        for row_i in range(self._model_model.rowCount()):
1649            func(row_i)
1650
1651    def updateModelFromList(self, param_dict):
1652        """
1653        Update the model with new parameters, create the errors column
1654        """
1655        assert isinstance(param_dict, dict)
1656        if not dict:
1657            return
1658
1659        def updateFittedValues(row):
1660            # Utility function for main model update
1661            # internal so can use closure for param_dict
1662            param_name = str(self._model_model.item(row, 0).text())
1663            if not self.isCheckable(row) or param_name not in list(param_dict.keys()):
1664                return
1665            # modify the param value
1666            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1667            self._model_model.item(row, 1).setText(param_repr)
1668            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1669            if self.has_error_column:
1670                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1671                self._model_model.item(row, 2).setText(error_repr)
1672
1673        def updatePolyValues(row):
1674            # Utility function for updateof polydispersity part of the main model
1675            param_name = str(self._model_model.item(row, 0).text())+'.width'
1676            if not self.isCheckable(row) or param_name not in list(param_dict.keys()):
1677                return
1678            # modify the param value
1679            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1680            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
1681            # modify the param error
1682            if self.has_error_column:
1683                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1684                self._model_model.item(row, 0).child(0).child(0,2).setText(error_repr)
1685
1686        def createErrorColumn(row):
1687            # Utility function for error column update
1688            item = QtGui.QStandardItem()
1689            def createItem(param_name):
1690                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1691                item.setText(error_repr)
1692            def curr_param():
1693                return str(self._model_model.item(row, 0).text())
1694
1695            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1696
1697            error_column.append(item)
1698
1699        def createPolyErrorColumn(row):
1700            # Utility function for error column update in the polydispersity sub-rows
1701            # NOTE: only creates empty items; updatePolyValues adds the error value
1702            item = self._model_model.item(row, 0)
1703            if not item.hasChildren():
1704                return
1705            poly_item = item.child(0)
1706            if not poly_item.hasChildren():
1707                return
1708            poly_item.insertColumn(2, [QtGui.QStandardItem("")])
1709
1710        if not self.has_error_column:
1711            # create top-level error column
1712            error_column = []
1713            self.lstParams.itemDelegate().addErrorColumn()
1714            self.iterateOverModel(createErrorColumn)
1715
1716            self._model_model.insertColumn(2, error_column)
1717
1718            FittingUtilities.addErrorHeadersToModel(self._model_model)
1719
1720            # create error column in polydispersity sub-rows
1721            self.iterateOverModel(createPolyErrorColumn)
1722
1723            self.has_error_column = True
1724
1725        # block signals temporarily, so we don't end up
1726        # updating charts with every single model change on the end of fitting
1727        self._model_model.dataChanged.disconnect()
1728        self.iterateOverModel(updateFittedValues)
1729        self.iterateOverModel(updatePolyValues)
1730        self._model_model.dataChanged.connect(self.onMainParamsChange)
1731
1732    def iterateOverPolyModel(self, func):
1733        """
1734        Take func and throw it inside the poly model row loop
1735        """
1736        for row_i in range(self._poly_model.rowCount()):
1737            func(row_i)
1738
1739    def updatePolyModelFromList(self, param_dict):
1740        """
1741        Update the polydispersity model with new parameters, create the errors column
1742        """
1743        assert isinstance(param_dict, dict)
1744        if not dict:
1745            return
1746
1747        def updateFittedValues(row_i):
1748            # Utility function for main model update
1749            # internal so can use closure for param_dict
1750            if row_i >= self._poly_model.rowCount():
1751                return
1752            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1753            if param_name not in list(param_dict.keys()):
1754                return
1755            # modify the param value
1756            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1757            self._poly_model.item(row_i, 1).setText(param_repr)
1758            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1759            if self.has_poly_error_column:
1760                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1761                self._poly_model.item(row_i, 2).setText(error_repr)
1762
1763        def createErrorColumn(row_i):
1764            # Utility function for error column update
1765            if row_i >= self._poly_model.rowCount():
1766                return
1767            item = QtGui.QStandardItem()
1768
1769            def createItem(param_name):
1770                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1771                item.setText(error_repr)
1772
1773            def poly_param():
1774                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1775
1776            [createItem(param_name) for param_name in list(param_dict.keys()) if poly_param() == param_name]
1777
1778            error_column.append(item)
1779
1780        # block signals temporarily, so we don't end up
1781        # updating charts with every single model change on the end of fitting
1782        self._poly_model.dataChanged.disconnect()
1783        self.iterateOverPolyModel(updateFittedValues)
1784        self._poly_model.dataChanged.connect(self.onPolyModelChange)
1785
1786        if self.has_poly_error_column:
1787            return
1788
1789        self.lstPoly.itemDelegate().addErrorColumn()
1790        error_column = []
1791        self.iterateOverPolyModel(createErrorColumn)
1792
1793        # switch off reponse to model change
1794        self._poly_model.insertColumn(2, error_column)
1795        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
1796
1797        self.has_poly_error_column = True
1798
1799    def iterateOverMagnetModel(self, func):
1800        """
1801        Take func and throw it inside the magnet model row loop
1802        """
1803        for row_i in range(self._magnet_model.rowCount()):
1804            func(row_i)
1805
1806    def updateMagnetModelFromList(self, param_dict):
1807        """
1808        Update the magnetic model with new parameters, create the errors column
1809        """
1810        assert isinstance(param_dict, dict)
1811        if not dict:
1812            return
1813        if self._magnet_model.rowCount() == 0:
1814            return
1815
1816        def updateFittedValues(row):
1817            # Utility function for main model update
1818            # internal so can use closure for param_dict
1819            if self._magnet_model.item(row, 0) is None:
1820                return
1821            param_name = str(self._magnet_model.item(row, 0).text())
1822            if param_name not in list(param_dict.keys()):
1823                return
1824            # modify the param value
1825            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1826            self._magnet_model.item(row, 1).setText(param_repr)
1827            self.kernel_module.setParam(param_name, param_dict[param_name][0])
1828            if self.has_magnet_error_column:
1829                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1830                self._magnet_model.item(row, 2).setText(error_repr)
1831
1832        def createErrorColumn(row):
1833            # Utility function for error column update
1834            item = QtGui.QStandardItem()
1835            def createItem(param_name):
1836                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1837                item.setText(error_repr)
1838            def curr_param():
1839                return str(self._magnet_model.item(row, 0).text())
1840
1841            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1842
1843            error_column.append(item)
1844
1845        # block signals temporarily, so we don't end up
1846        # updating charts with every single model change on the end of fitting
1847        self._magnet_model.dataChanged.disconnect()
1848        self.iterateOverMagnetModel(updateFittedValues)
1849        self._magnet_model.dataChanged.connect(self.onMagnetModelChange)
1850
1851        if self.has_magnet_error_column:
1852            return
1853
1854        self.lstMagnetic.itemDelegate().addErrorColumn()
1855        error_column = []
1856        self.iterateOverMagnetModel(createErrorColumn)
1857
1858        # switch off reponse to model change
1859        self._magnet_model.insertColumn(2, error_column)
1860        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1861
1862        self.has_magnet_error_column = True
1863
1864    def onPlot(self):
1865        """
1866        Plot the current set of data
1867        """
1868        # Regardless of previous state, this should now be `plot show` functionality only
1869        self.cmdPlot.setText("Show Plot")
1870        # Force data recalculation so existing charts are updated
1871        if not self.data_is_loaded:
1872            self.showTheoryPlot()
1873        else:
1874            self.showPlot()
1875        # This is an important processEvent.
1876        # This allows charts to be properly updated in order
1877        # of plots being applied.
1878        QtWidgets.QApplication.processEvents()
1879        self.recalculatePlotData() # recalc+plot theory again (2nd)
1880
1881    def onSmearingOptionsUpdate(self):
1882        """
1883        React to changes in the smearing widget
1884        """
1885        self.calculateQGridForModel()
1886
1887    def recalculatePlotData(self):
1888        """
1889        Generate a new dataset for model
1890        """
1891        if not self.data_is_loaded:
1892            self.createDefaultDataset()
1893        self.calculateQGridForModel()
1894
1895    def showTheoryPlot(self):
1896        """
1897        Show the current theory plot in MPL
1898        """
1899        # Show the chart if ready
1900        if self.theory_item is None:
1901            self.recalculatePlotData()
1902        elif self.model_data:
1903            self._requestPlots(self.model_data.filename, self.theory_item.model())
1904
1905    def showPlot(self):
1906        """
1907        Show the current plot in MPL
1908        """
1909        # Show the chart if ready
1910        data_to_show = self.data
1911        # Any models for this page
1912        current_index = self.all_data[self.data_index]
1913        item = self._requestPlots(self.data.filename, current_index.model())
1914        if item:
1915            # fit+data has not been shown - show just data
1916            self.communicate.plotRequestedSignal.emit([item, data_to_show], self.tab_id)
1917
1918    def _requestPlots(self, item_name, item_model):
1919        """
1920        Emits plotRequestedSignal for all plots found in the given model under the provided item name.
1921        """
1922        fitpage_name = "" if self.tab_id is None else "M"+str(self.tab_id)
1923        plots = GuiUtils.plotsFromFilename(item_name, item_model)
1924        # Has the fitted data been shown?
1925        data_shown = False
1926        item = None
1927        for item, plot in plots.items():
1928            if fitpage_name in plot.name:
1929                data_shown = True
1930                self.communicate.plotRequestedSignal.emit([item, plot], self.tab_id)
1931        # return the last data item seen, if nothing was plotted; supposed to be just data)
1932        return None if data_shown else item
1933
1934    def onOptionsUpdate(self):
1935        """
1936        Update local option values and replot
1937        """
1938        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1939            self.options_widget.state()
1940        # set Q range labels on the main tab
1941        self.lblMinRangeDef.setText(GuiUtils.formatNumber(self.q_range_min, high=True))
1942        self.lblMaxRangeDef.setText(GuiUtils.formatNumber(self.q_range_max, high=True))
1943        self.recalculatePlotData()
1944
1945    def setDefaultStructureCombo(self):
1946        """
1947        Fill in the structure factors combo box with defaults
1948        """
1949        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1950        factors = [factor[0] for factor in structure_factor_list]
1951        factors.insert(0, STRUCTURE_DEFAULT)
1952        self.cbStructureFactor.clear()
1953        self.cbStructureFactor.addItems(sorted(factors))
1954
1955    def createDefaultDataset(self):
1956        """
1957        Generate default Dataset 1D/2D for the given model
1958        """
1959        # Create default datasets if no data passed
1960        if self.is2D:
1961            qmax = self.q_range_max/np.sqrt(2)
1962            qstep = self.npts
1963            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
1964            return
1965        elif self.log_points:
1966            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
1967            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
1968            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
1969        else:
1970            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
1971                                   num=self.npts, endpoint=True)
1972        self.logic.createDefault1dData(interval, self.tab_id)
1973
1974    def readCategoryInfo(self):
1975        """
1976        Reads the categories in from file
1977        """
1978        self.master_category_dict = defaultdict(list)
1979        self.by_model_dict = defaultdict(list)
1980        self.model_enabled_dict = defaultdict(bool)
1981
1982        categorization_file = CategoryInstaller.get_user_file()
1983        if not os.path.isfile(categorization_file):
1984            categorization_file = CategoryInstaller.get_default_file()
1985        with open(categorization_file, 'rb') as cat_file:
1986            self.master_category_dict = json.load(cat_file)
1987            self.regenerateModelDict()
1988
1989        # Load the model dict
1990        models = load_standard_models()
1991        for model in models:
1992            self.models[model.name] = model
1993
1994        self.readCustomCategoryInfo()
1995
1996    def readCustomCategoryInfo(self):
1997        """
1998        Reads the custom model category
1999        """
2000        #Looking for plugins
2001        self.plugins = list(self.custom_models.values())
2002        plugin_list = []
2003        for name, plug in self.custom_models.items():
2004            self.models[name] = plug
2005            plugin_list.append([name, True])
2006        if plugin_list:
2007            self.master_category_dict[CATEGORY_CUSTOM] = plugin_list
2008
2009    def regenerateModelDict(self):
2010        """
2011        Regenerates self.by_model_dict which has each model name as the
2012        key and the list of categories belonging to that model
2013        along with the enabled mapping
2014        """
2015        self.by_model_dict = defaultdict(list)
2016        for category in self.master_category_dict:
2017            for (model, enabled) in self.master_category_dict[category]:
2018                self.by_model_dict[model].append(category)
2019                self.model_enabled_dict[model] = enabled
2020
2021    def addBackgroundToModel(self, model):
2022        """
2023        Adds background parameter with default values to the model
2024        """
2025        assert isinstance(model, QtGui.QStandardItemModel)
2026        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
2027        FittingUtilities.addCheckedListToModel(model, checked_list)
2028        last_row = model.rowCount()-1
2029        model.item(last_row, 0).setEditable(False)
2030        model.item(last_row, 4).setEditable(False)
2031
2032    def addScaleToModel(self, model):
2033        """
2034        Adds scale parameter with default values to the model
2035        """
2036        assert isinstance(model, QtGui.QStandardItemModel)
2037        checked_list = ['scale', '1.0', '0.0', 'inf', '']
2038        FittingUtilities.addCheckedListToModel(model, checked_list)
2039        last_row = model.rowCount()-1
2040        model.item(last_row, 0).setEditable(False)
2041        model.item(last_row, 4).setEditable(False)
2042
2043    def addWeightingToData(self, data):
2044        """
2045        Adds weighting contribution to fitting data
2046        """
2047        new_data = copy.deepcopy(data)
2048        # Send original data for weighting
2049        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2050        if self.is2D:
2051            new_data.err_data = weight
2052        else:
2053            new_data.dy = weight
2054
2055        return new_data
2056
2057    def updateQRange(self):
2058        """
2059        Updates Q Range display
2060        """
2061        if self.data_is_loaded:
2062            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
2063        # set Q range labels on the main tab
2064        self.lblMinRangeDef.setText(GuiUtils.formatNumber(self.q_range_min, high=True))
2065        self.lblMaxRangeDef.setText(GuiUtils.formatNumber(self.q_range_max, high=True))
2066        # set Q range labels on the options tab
2067        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
2068
2069    def SASModelToQModel(self, model_name, structure_factor=None):
2070        """
2071        Setting model parameters into table based on selected category
2072        """
2073        # Crete/overwrite model items
2074        self._model_model.clear()
2075        self._poly_model.clear()
2076        self._magnet_model.clear()
2077
2078        if model_name is None:
2079            if structure_factor not in (None, "None"):
2080                # S(Q) on its own, treat the same as a form factor
2081                self.kernel_module = None
2082                self.fromStructureFactorToQModel(structure_factor)
2083            else:
2084                # No models selected
2085                return
2086        else:
2087            self.fromModelToQModel(model_name)
2088            self.addExtraShells()
2089
2090            # Allow the SF combobox visibility for the given sasmodel
2091            self.enableStructureFactorControl(structure_factor)
2092       
2093            # Add S(Q)
2094            if self.cbStructureFactor.isEnabled():
2095                structure_factor = self.cbStructureFactor.currentText()
2096                self.fromStructureFactorToQModel(structure_factor)
2097
2098            # Add polydispersity to the model
2099            self.poly_params = {}
2100            self.setPolyModel()
2101            # Add magnetic parameters to the model
2102            self.magnet_params = {}
2103            self.setMagneticModel()
2104
2105        # Now we claim the model has been loaded
2106        self.model_is_loaded = True
2107        # Change the model name to a monicker
2108        self.kernel_module.name = self.modelName()
2109        # Update the smearing tab
2110        self.smearing_widget.updateKernelModel(kernel_model=self.kernel_module)
2111
2112        # (Re)-create headers
2113        FittingUtilities.addHeadersToModel(self._model_model)
2114        self.lstParams.header().setFont(self.boldFont)
2115
2116        # Update Q Ranges
2117        self.updateQRange()
2118
2119    def fromModelToQModel(self, model_name):
2120        """
2121        Setting model parameters into QStandardItemModel based on selected _model_
2122        """
2123        name = model_name
2124        kernel_module = None
2125        if self.cbCategory.currentText() == CATEGORY_CUSTOM:
2126            # custom kernel load requires full path
2127            name = os.path.join(ModelUtilities.find_plugins_dir(), model_name+".py")
2128        try:
2129            kernel_module = generate.load_kernel_module(name)
2130        except ModuleNotFoundError as ex:
2131            pass
2132        except FileNotFoundError as ex:
2133            # can happen when name attribute not the same as actual filename
2134            pass
2135
2136        if kernel_module is None:
2137            # mismatch between "name" attribute and actual filename.
2138            curr_model = self.models[model_name]
2139            name, _ = os.path.splitext(os.path.basename(curr_model.filename))
2140            try:
2141                kernel_module = generate.load_kernel_module(name)
2142            except ModuleNotFoundError as ex:
2143                logger.error("Can't find the model "+ str(ex))
2144                return
2145
2146        if hasattr(kernel_module, 'parameters'):
2147            # built-in and custom models
2148            self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
2149
2150        elif hasattr(kernel_module, 'model_info'):
2151            # for sum/multiply models
2152            self.model_parameters = kernel_module.model_info.parameters
2153
2154        elif hasattr(kernel_module, 'Model') and hasattr(kernel_module.Model, "_model_info"):
2155            # this probably won't work if there's no model_info, but just in case
2156            self.model_parameters = kernel_module.Model._model_info.parameters
2157        else:
2158            # no parameters - default to blank table
2159            msg = "No parameters found in model '{}'.".format(model_name)
2160            logger.warning(msg)
2161            self.model_parameters = modelinfo.ParameterTable([])
2162
2163        # Instantiate the current sasmodel
2164        self.kernel_module = self.models[model_name]()
2165
2166        # Change the model name to a monicker
2167        self.kernel_module.name = self.modelName()
2168
2169        # Explicitly add scale and background with default values
2170        temp_undo_state = self.undo_supported
2171        self.undo_supported = False
2172        self.addScaleToModel(self._model_model)
2173        self.addBackgroundToModel(self._model_model)
2174        self.undo_supported = temp_undo_state
2175
2176        self.shell_names = self.shellNamesList()
2177
2178        # Add heading row
2179        FittingUtilities.addHeadingRowToModel(self._model_model, model_name)
2180
2181        # Update the QModel
2182        FittingUtilities.addParametersToModel(
2183                self.model_parameters,
2184                self.kernel_module,
2185                self.is2D,
2186                self._model_model,
2187                self.lstParams)
2188
2189    def fromStructureFactorToQModel(self, structure_factor):
2190        """
2191        Setting model parameters into QStandardItemModel based on selected _structure factor_
2192        """
2193        if structure_factor is None or structure_factor=="None":
2194            return
2195
2196        product_params = None
2197
2198        if self.kernel_module is None:
2199            # Structure factor is the only selected model; build it and show all its params
2200            self.kernel_module = self.models[structure_factor]()
2201            self.kernel_module.name = self.modelName()
2202            s_params = self.kernel_module._model_info.parameters
2203            s_params_orig = s_params
2204        else:
2205            s_kernel = self.models[structure_factor]()
2206            p_kernel = self.kernel_module
2207            # need to reset multiplicity to get the right product
2208            if p_kernel.is_multiplicity_model:
2209                p_kernel.multiplicity = p_kernel.multiplicity_info.number
2210
2211            p_pars_len = len(p_kernel._model_info.parameters.kernel_parameters)
2212            s_pars_len = len(s_kernel._model_info.parameters.kernel_parameters)
2213
2214            self.kernel_module = MultiplicationModel(p_kernel, s_kernel)
2215            # Modify the name to correspond to shown items
2216            self.kernel_module.name = self.modelName()
2217            all_params = self.kernel_module._model_info.parameters.kernel_parameters
2218            all_param_names = [param.name for param in all_params]
2219
2220            # S(Q) params from the product model are not necessarily the same as those from the S(Q) model; any
2221            # conflicting names with P(Q) params will cause a rename
2222
2223            if "radius_effective_mode" in all_param_names:
2224                # Show all parameters
2225                # In this case, radius_effective is NOT pruned by sasmodels.product
2226                s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len])
2227                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters)
2228                product_params = modelinfo.ParameterTable(
2229                        self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
2230            else:
2231                # Ensure radius_effective is not displayed
2232                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters[1:])
2233                if "radius_effective" in all_param_names:
2234                    # In this case, radius_effective is NOT pruned by sasmodels.product
2235                    s_params = modelinfo.ParameterTable(all_params[p_pars_len+1:p_pars_len+s_pars_len])
2236                    product_params = modelinfo.ParameterTable(
2237                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
2238                else:
2239                    # In this case, radius_effective is pruned by sasmodels.product
2240                    s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len-1])
2241                    product_params = modelinfo.ParameterTable(
2242                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len-1:])
2243
2244        # Add heading row
2245        FittingUtilities.addHeadingRowToModel(self._model_model, structure_factor)
2246
2247        # Get new rows for QModel
2248        # Any renamed parameters are stored as data in the relevant item, for later handling
2249        FittingUtilities.addSimpleParametersToModel(
2250                parameters=s_params,
2251                is2D=self.is2D,
2252                parameters_original=s_params_orig,
2253                model=self._model_model,
2254                view=self.lstParams)
2255
2256        # Insert product-only params into QModel
2257        if product_params:
2258            prod_rows = FittingUtilities.addSimpleParametersToModel(
2259                    parameters=product_params,
2260                    is2D=self.is2D,
2261                    parameters_original=None,
2262                    model=self._model_model,
2263                    view=self.lstParams,
2264                    row_num=2)
2265
2266            # Since this all happens after shells are dealt with and we've inserted rows, fix this counter
2267            self._n_shells_row += len(prod_rows)
2268
2269    def haveParamsToFit(self):
2270        """
2271        Finds out if there are any parameters ready to be fitted
2272        """
2273        if not self.logic.data_is_loaded:
2274            return False
2275        if self.main_params_to_fit:
2276            return True
2277        if self.chkPolydispersity.isChecked() and self.poly_params_to_fit:
2278            return True
2279        if self.chkMagnetism.isChecked() and self.canHaveMagnetism() and self.magnet_params_to_fit:
2280            return True
2281        return False
2282
2283    def onMainParamsChange(self, top, bottom):
2284        """
2285        Callback method for updating the sasmodel parameters with the GUI values
2286        """
2287        item = self._model_model.itemFromIndex(top)
2288
2289        model_column = item.column()
2290
2291        if model_column == 0:
2292            self.checkboxSelected(item)
2293            self.cmdFit.setEnabled(self.haveParamsToFit())
2294            # Update state stack
2295            self.updateUndo()
2296            return
2297
2298        model_row = item.row()
2299        name_index = self._model_model.index(model_row, 0)
2300        name_item = self._model_model.itemFromIndex(name_index)
2301
2302        # Extract changed value.
2303        try:
2304            value = GuiUtils.toDouble(item.text())
2305        except TypeError:
2306            # Unparsable field
2307            return
2308
2309        # if the item has user data, this is the actual parameter name (e.g. to handle duplicate names)
2310        if name_item.data(QtCore.Qt.UserRole):
2311            parameter_name = str(name_item.data(QtCore.Qt.UserRole))
2312        else:
2313            parameter_name = str(self._model_model.data(name_index))
2314
2315        # Update the parameter value - note: this supports +/-inf as well
2316        param_column = self.lstParams.itemDelegate().param_value
2317        min_column = self.lstParams.itemDelegate().param_min
2318        max_column = self.lstParams.itemDelegate().param_max
2319        if model_column == param_column:
2320            self.kernel_module.setParam(parameter_name, value)
2321        elif model_column == min_column:
2322            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
2323            self.kernel_module.details[parameter_name][1] = value
2324        elif model_column == max_column:
2325            self.kernel_module.details[parameter_name][2] = value
2326        else:
2327            # don't update the chart
2328            return
2329
2330        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
2331        # TODO: multishell params in self.kernel_module.details[??] = value
2332
2333        # handle display of effective radius parameter according to radius_effective_mode; pass ER into model if
2334        # necessary
2335        self.processEffectiveRadius()
2336
2337        # Force the chart update when actual parameters changed
2338        if model_column == 1:
2339            self.recalculatePlotData()
2340
2341        # Update state stack
2342        self.updateUndo()
2343
2344    def processEffectiveRadius(self):
2345        """
2346        Checks the value of radius_effective_mode, if existent, and processes radius_effective as necessary.
2347        * mode == 0: This means 'unconstrained'; ensure use can specify ER.
2348        * mode > 0: This means it is constrained to a P(Q)-computed value in sasmodels; prevent user from editing ER.
2349
2350        Note: If ER has been computed, it is passed back to SasView as an intermediate result. That value must be
2351        displayed for the user; that is not dealt with here, but in complete1D.
2352        """
2353        ER_row = self.getRowFromName("radius_effective")
2354        if ER_row is None:
2355            return
2356
2357        ER_mode_row = self.getRowFromName("radius_effective_mode")
2358        if ER_mode_row is None:
2359            return
2360
2361        try:
2362            ER_mode = int(self._model_model.item(ER_mode_row, 1).text())
2363        except ValueError:
2364            logging.error("radius_effective_mode was set to an invalid value.")
2365            return
2366
2367        if ER_mode == 0:
2368            # ensure the ER value can be modified by user
2369            self.setParamEditableByRow(ER_row, True)
2370        elif ER_mode > 0:
2371            # ensure the ER value cannot be modified by user
2372            self.setParamEditableByRow(ER_row, False)
2373        else:
2374            logging.error("radius_effective_mode was set to an invalid value.")
2375
2376    def setParamEditableByRow(self, row, editable=True):
2377        """
2378        Sets whether the user can edit a parameter in the table. If they cannot, the parameter name's font is changed,
2379        the value itself cannot be edited if clicked on, and the parameter may not be fitted.
2380        """
2381        item_name = self._model_model.item(row, 0)
2382        item_value = self._model_model.item(row, 1)
2383
2384        item_value.setEditable(editable)
2385
2386        if editable:
2387            # reset font
2388            item_name.setFont(QtGui.QFont())
2389            # reset colour
2390            item_name.setForeground(QtGui.QBrush())
2391            # make checkable
2392            item_name.setCheckable(True)
2393        else:
2394            # change font
2395            font = QtGui.QFont()
2396            font.setItalic(True)
2397            item_name.setFont(font)
2398            # change colour
2399            item_name.setForeground(QtGui.QBrush(QtGui.QColor(50, 50, 50)))
2400            # make not checkable (and uncheck)
2401            item_name.setCheckState(QtCore.Qt.Unchecked)
2402            item_name.setCheckable(False)
2403
2404    def isCheckable(self, row):
2405        return self._model_model.item(row, 0).isCheckable()
2406
2407    def checkboxSelected(self, item):
2408        # Assure we're dealing with checkboxes
2409        if not item.isCheckable():
2410            return
2411        status = item.checkState()
2412
2413        # If multiple rows selected - toggle all of them, filtering uncheckable
2414        # Switch off signaling from the model to avoid recursion
2415        self._model_model.blockSignals(True)
2416        # Convert to proper indices and set requested enablement
2417        self.setParameterSelection(status)
2418        self._model_model.blockSignals(False)
2419
2420        # update the list of parameters to fit
2421        self.main_params_to_fit = self.checkedListFromModel(self._model_model)
2422
2423    def checkedListFromModel(self, model):
2424        """
2425        Returns list of checked parameters for given model
2426        """
2427        def isChecked(row):
2428            return model.item(row, 0).checkState() == QtCore.Qt.Checked
2429
2430        return [str(model.item(row_index, 0).text())
2431                for row_index in range(model.rowCount())
2432                if isChecked(row_index)]
2433
2434    def createNewIndex(self, fitted_data):
2435        """
2436        Create a model or theory index with passed Data1D/Data2D
2437        """
2438        if self.data_is_loaded:
2439            if not fitted_data.name:
2440                name = self.nameForFittedData(self.data.filename)
2441                fitted_data.title = name
2442                fitted_data.name = name
2443                fitted_data.filename = name
2444                fitted_data.symbol = "Line"
2445            self.updateModelIndex(fitted_data)
2446        else:
2447            if not fitted_data.name:
2448                name = self.nameForFittedData(self.kernel_module.id)
2449            else:
2450                name = fitted_data.name
2451            fitted_data.title = name
2452            fitted_data.filename = name
2453            fitted_data.symbol = "Line"
2454            self.createTheoryIndex(fitted_data)
2455            # Switch to the theory tab for user's glee
2456            self.communicate.changeDataExplorerTabSignal.emit(1)
2457
2458    def updateModelIndex(self, fitted_data):
2459        """
2460        Update a QStandardModelIndex containing model data
2461        """
2462        name = self.nameFromData(fitted_data)
2463        # Make this a line if no other defined
2464        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
2465            fitted_data.symbol = 'Line'
2466        # Notify the GUI manager so it can update the main model in DataExplorer
2467        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
2468
2469    def createTheoryIndex(self, fitted_data):
2470        """
2471        Create a QStandardModelIndex containing model data
2472        """
2473        name = self.nameFromData(fitted_data)
2474        # Notify the GUI manager so it can create the theory model in DataExplorer
2475        self.theory_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
2476        self.communicate.updateTheoryFromPerspectiveSignal.emit(self.theory_item)
2477
2478    def nameFromData(self, fitted_data):
2479        """
2480        Return name for the dataset. Terribly impure function.
2481        """
2482        if fitted_data.name is None:
2483            name = self.nameForFittedData(self.logic.data.filename)
2484            fitted_data.title = name
2485            fitted_data.name = name
2486            fitted_data.filename = name
2487        else:
2488            name = fitted_data.name
2489        return name
2490
2491    def methodCalculateForData(self):
2492        '''return the method for data calculation'''
2493        return Calc1D if isinstance(self.data, Data1D) else Calc2D
2494
2495    def methodCompleteForData(self):
2496        '''return the method for result parsin on calc complete '''
2497        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
2498
2499    def updateKernelModelWithExtraParams(self, model=None):
2500        """
2501        Updates kernel model 'model' with extra parameters from
2502        the polydisp and magnetism tab, if the tabs are enabled
2503        """
2504        if model is None: return
2505        if not hasattr(model, 'setParam'): return
2506
2507        # add polydisperse parameters if asked
2508        if self.chkPolydispersity.isChecked() and self._poly_model.rowCount() > 0:
2509            for key, value in self.poly_params.items():
2510                model.setParam(key, value)
2511        # add magnetic params if asked
2512        if self.chkMagnetism.isChecked() and self.canHaveMagnetism() and self._magnet_model.rowCount() > 0:
2513            for key, value in self.magnet_params.items():
2514                model.setParam(key, value)
2515
2516    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
2517        """
2518        Wrapper for Calc1D/2D calls
2519        """
2520        if data is None:
2521            data = self.data
2522        if model is None:
2523            model = copy.deepcopy(self.kernel_module)
2524            self.updateKernelModelWithExtraParams(model)
2525
2526        if completefn is None:
2527            completefn = self.methodCompleteForData()
2528        smearer = self.smearing_widget.smearer()
2529        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2530
2531        # Disable buttons/table
2532        self.disableInteractiveElements()
2533        # Awful API to a backend method.
2534        calc_thread = self.methodCalculateForData()(data=data,
2535                                               model=model,
2536                                               page_id=0,
2537                                               qmin=self.q_range_min,
2538                                               qmax=self.q_range_max,
2539                                               smearer=smearer,
2540                                               state=None,
2541                                               weight=weight,
2542                                               fid=None,
2543                                               toggle_mode_on=False,
2544                                               completefn=completefn,
2545                                               update_chisqr=True,
2546                                               exception_handler=self.calcException,
2547                                               source=None)
2548        if use_threads:
2549            if LocalConfig.USING_TWISTED:
2550                # start the thread with twisted
2551                thread = threads.deferToThread(calc_thread.compute)
2552                thread.addCallback(completefn)
2553                thread.addErrback(self.calculateDataFailed)
2554            else:
2555                # Use the old python threads + Queue
2556                calc_thread.queue()
2557                calc_thread.ready(2.5)
2558        else:
2559            results = calc_thread.compute()
2560            completefn(results)
2561
2562    def calculateQGridForModel(self):
2563        """
2564        Prepare the fitting data object, based on current ModelModel
2565        """
2566        if self.kernel_module is None:
2567            return
2568        self.calculateQGridForModelExt()
2569
2570    def calculateDataFailed(self, reason):
2571        """
2572        Thread returned error
2573        """
2574        # Bring the GUI to normal state
2575        self.enableInteractiveElements()
2576        print("Calculate Data failed with ", reason)
2577
2578    def completed1D(self, return_data):
2579        self.Calc1DFinishedSignal.emit(return_data)
2580
2581    def completed2D(self, return_data):
2582        self.Calc2DFinishedSignal.emit(return_data)
2583
2584    def complete1D(self, return_data):
2585        """
2586        Plot the current 1D data
2587        """
2588        # Bring the GUI to normal state
2589        self.enableInteractiveElements()
2590        if return_data is None:
2591            return
2592        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
2593
2594        # assure the current index is set properly for batch
2595        if len(self._logic) > 1:
2596            for i, logic in enumerate(self._logic):
2597                if logic.data.name in fitted_data.name:
2598                    self.data_index = i
2599
2600        residuals = self.calculateResiduals(fitted_data)
2601        self.model_data = fitted_data
2602        new_plots = [fitted_data]
2603        if residuals is not None:
2604            new_plots.append(residuals)
2605
2606        if self.data_is_loaded:
2607            # delete any plots associated with the data that were not updated
2608            # (e.g. to remove beta(Q), S_eff(Q))
2609            GuiUtils.deleteRedundantPlots(self.all_data[self.data_index], new_plots)
2610            pass
2611        else:
2612            # delete theory items for the model, in order to get rid of any
2613            # redundant items, e.g. beta(Q), S_eff(Q)
2614            self.communicate.deleteIntermediateTheoryPlotsSignal.emit(self.kernel_module.id)
2615
2616        # Create plots for parameters with enabled polydispersity
2617        for plot in FittingUtilities.plotPolydispersities(return_data.get('model', None)):
2618            data_id = fitted_data.id.split()
2619            plot.id = "{} [{}] {}".format(data_id[0], plot.name, " ".join(data_id[1:]))
2620            data_name = fitted_data.name.split()
2621            plot.name = " ".join([data_name[0], plot.name] + data_name[1:])
2622            self.createNewIndex(plot)
2623            new_plots.append(plot)
2624
2625        # Create plots for intermediate product data
2626        plots = self.logic.new1DProductPlots(return_data, self.tab_id)
2627        for plot in plots:
2628            plot.symbol = "Line"
2629            self.createNewIndex(plot)
2630            new_plots.append(plot)
2631
2632        for plot in new_plots:
2633            self.communicate.plotUpdateSignal.emit([plot])
2634
2635        # Update radius_effective if relevant
2636        self.updateEffectiveRadius(return_data)
2637
2638    def complete2D(self, return_data):
2639        """
2640        Plot the current 2D data
2641        """
2642        # Bring the GUI to normal state
2643        self.enableInteractiveElements()
2644
2645        if return_data is None:
2646            return
2647
2648        fitted_data = self.logic.new2DPlot(return_data)
2649        # assure the current index is set properly for batch
2650        if len(self._logic) > 1:
2651            for i, logic in enumerate(self._logic):
2652                if logic.data.name in fitted_data.name:
2653                    self.data_index = i
2654
2655        residuals = self.calculateResiduals(fitted_data)
2656        self.model_data = fitted_data
2657        new_plots = [fitted_data]
2658        if residuals is not None:
2659            new_plots.append(residuals)
2660
2661        # Update/generate plots
2662        for plot in new_plots:
2663            self.communicate.plotUpdateSignal.emit([plot])
2664
2665    def updateEffectiveRadius(self, return_data):
2666        """
2667        Given return data from sasmodels, update the effective radius parameter in the GUI table with the new
2668        calculated value as returned by sasmodels (if the value was returned).
2669        """
2670        ER_mode_row = self.getRowFromName("radius_effective_mode")
2671        if ER_mode_row is None:
2672            return
2673        try:
2674            ER_mode = int(self._model_model.item(ER_mode_row, 1).text())
2675        except ValueError:
2676            logging.error("radius_effective_mode was set to an invalid value.")
2677            return
2678        if ER_mode < 1:
2679            # does not need updating if it is not being computed
2680            return
2681
2682        ER_row = self.getRowFromName("radius_effective")
2683        if ER_row is None:
2684            return
2685
2686        scalar_results = self.logic.getScalarIntermediateResults(return_data)
2687        ER_value = scalar_results.get("effective_radius") # note name of key
2688        if ER_value is None:
2689            return
2690        # ensure the model does not recompute when updating the value
2691        self._model_model.blockSignals(True)
2692        self._model_model.item(ER_row, 1).setText(str(ER_value))
2693        self._model_model.blockSignals(False)
2694        # ensure the view is updated immediately
2695        self._model_model.layoutChanged.emit()
2696
2697    def calculateResiduals(self, fitted_data):
2698        """
2699        Calculate and print Chi2 and display chart of residuals. Returns residuals plot object.
2700        """
2701        # Create a new index for holding data
2702        fitted_data.symbol = "Line"
2703
2704        # Modify fitted_data with weighting
2705        weighted_data = self.addWeightingToData(fitted_data)
2706
2707        self.createNewIndex(weighted_data)
2708
2709        # Calculate difference between return_data and logic.data
2710        self.chi2 = FittingUtilities.calculateChi2(weighted_data, self.data)
2711        # Update the control
2712        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
2713        self.lblChi2Value.setText(chi2_repr)
2714
2715        # Plot residuals if actual data
2716        if not self.data_is_loaded:
2717            return
2718
2719        residuals_plot = FittingUtilities.plotResiduals(self.data, weighted_data)
2720        if residuals_plot is None:
2721            return
2722        residuals_plot.id = "Residual " + residuals_plot.id
2723        residuals_plot.plot_role = Data1D.ROLE_RESIDUAL
2724        self.createNewIndex(residuals_plot)
2725        return residuals_plot
2726
2727    def onCategoriesChanged(self):
2728            """
2729            Reload the category/model comboboxes
2730            """
2731            # Store the current combo indices
2732            current_cat = self.cbCategory.currentText()
2733            current_model = self.cbModel.currentText()
2734
2735            # reread the category file and repopulate the combo
2736            self.cbCategory.blockSignals(True)
2737            self.cbCategory.clear()
2738            self.readCategoryInfo()
2739            self.initializeCategoryCombo()
2740
2741            # Scroll back to the original index in Categories
2742            new_index = self.cbCategory.findText(current_cat)
2743            if new_index != -1:
2744                self.cbCategory.setCurrentIndex(new_index)
2745            self.cbCategory.blockSignals(False)
2746            # ...and in the Models
2747            self.cbModel.blockSignals(True)
2748            new_index = self.cbModel.findText(current_model)
2749            if new_index != -1:
2750                self.cbModel.setCurrentIndex(new_index)
2751            self.cbModel.blockSignals(False)
2752
2753            return
2754
2755    def calcException(self, etype, value, tb):
2756        """
2757        Thread threw an exception.
2758        """
2759        # Bring the GUI to normal state
2760        self.enableInteractiveElements()
2761        # TODO: remimplement thread cancellation
2762        logger.error("".join(traceback.format_exception(etype, value, tb)))
2763
2764    def setTableProperties(self, table):
2765        """
2766        Setting table properties
2767        """
2768        # Table properties
2769        table.verticalHeader().setVisible(False)
2770        table.setAlternatingRowColors(True)
2771        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2772        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
2773        table.resizeColumnsToContents()
2774
2775        # Header
2776        header = table.horizontalHeader()
2777        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2778        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
2779
2780        # Qt5: the following 2 lines crash - figure out why!
2781        # Resize column 0 and 7 to content
2782        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2783        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
2784
2785    def setPolyModel(self):
2786        """
2787        Set polydispersity values
2788        """
2789        if not self.model_parameters:
2790            return
2791        self._poly_model.clear()
2792
2793        parameters = self.model_parameters.form_volume_parameters
2794        if self.is2D:
2795            parameters += self.model_parameters.orientation_parameters
2796
2797        [self.setPolyModelParameters(i, param) for i, param in \
2798            enumerate(parameters) if param.polydisperse]
2799
2800        FittingUtilities.addPolyHeadersToModel(self._poly_model)
2801
2802    def setPolyModelParameters(self, i, param):
2803        """
2804        Standard of multishell poly parameter driver
2805        """
2806        param_name = param.name
2807        # see it the parameter is multishell
2808        if '[' in param.name:
2809            # Skip empty shells
2810            if self.current_shell_displayed == 0:
2811                return
2812            else:
2813                # Create as many entries as current shells
2814                for ishell in range(1, self.current_shell_displayed+1):
2815                    # Remove [n] and add the shell numeral
2816                    name = param_name[0:param_name.index('[')] + str(ishell)
2817                    self.addNameToPolyModel(i, name)
2818        else:
2819            # Just create a simple param entry
2820            self.addNameToPolyModel(i, param_name)
2821
2822    def addNameToPolyModel(self, i, param_name):
2823        """
2824        Creates a checked row in the poly model with param_name
2825        """
2826        # Polydisp. values from the sasmodel
2827        width = self.kernel_module.getParam(param_name + '.width')
2828        npts = self.kernel_module.getParam(param_name + '.npts')
2829        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2830        _, min, max = self.kernel_module.details[param_name]
2831
2832        # Update local param dict
2833        self.poly_params[param_name + '.width'] = width
2834        self.poly_params[param_name + '.npts'] = npts
2835        self.poly_params[param_name + '.nsigmas'] = nsigs
2836
2837        # Construct a row with polydisp. related variable.
2838        # This will get added to the polydisp. model
2839        # Note: last argument needs extra space padding for decent display of the control
2840        checked_list = ["Distribution of " + param_name, str(width),
2841                        str(min), str(max),
2842                        str(npts), str(nsigs), "gaussian      ",'']
2843        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2844
2845        # All possible polydisp. functions as strings in combobox
2846        func = QtWidgets.QComboBox()
2847        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
2848        # Set the default index
2849        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
2850        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2851        self.lstPoly.setIndexWidget(ind, func)
2852        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2853
2854    def onPolyFilenameChange(self, row_index):
2855        """
2856        Respond to filename_updated signal from the delegate
2857        """
2858        # For the given row, invoke the "array" combo handler
2859        array_caption = 'array'
2860
2861        # Get the combo box reference
2862        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2863        widget = self.lstPoly.indexWidget(ind)
2864
2865        # Update the combo box so it displays "array"
2866        widget.blockSignals(True)
2867        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
2868        widget.blockSignals(False)
2869
2870        # Invoke the file reader
2871        self.onPolyComboIndexChange(array_caption, row_index)
2872
2873    def onPolyComboIndexChange(self, combo_string, row_index):
2874        """
2875        Modify polydisp. defaults on function choice
2876        """
2877        # Get npts/nsigs for current selection
2878        param = self.model_parameters.form_volume_parameters[row_index]
2879        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2880        combo_box = self.lstPoly.indexWidget(file_index)
2881
2882        def updateFunctionCaption(row):
2883            # Utility function for update of polydispersity function name in the main model
2884            if not self.isCheckable(row):
2885                return
2886            self._model_model.blockSignals(True)
2887            param_name = str(self._model_model.item(row, 0).text())
2888            self._model_model.blockSignals(False)
2889            if param_name !=  param.name:
2890                return
2891            # Modify the param value
2892            self._model_model.blockSignals(True)
2893            if self.has_error_column:
2894                # err column changes the indexing
2895                self._model_model.item(row, 0).child(0).child(0,5).setText(combo_string)
2896            else:
2897                self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
2898            self._model_model.blockSignals(False)
2899
2900        if combo_string == 'array':
2901            try:
2902                self.loadPolydispArray(row_index)
2903                # Update main model for display
2904                self.iterateOverModel(updateFunctionCaption)
2905                # disable the row
2906                lo = self.lstPoly.itemDelegate().poly_pd
2907                hi = self.lstPoly.itemDelegate().poly_function
2908                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
2909                return
2910            except IOError:
2911                combo_box.setCurrentIndex(self.orig_poly_index)
2912                # Pass for cancel/bad read
2913                pass
2914
2915        # Enable the row in case it was disabled by Array
2916        self._poly_model.blockSignals(True)
2917        max_range = self.lstPoly.itemDelegate().poly_filename
2918        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
2919        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2920        self._poly_model.setData(file_index, "")
2921        self._poly_model.blockSignals(False)
2922
2923        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
2924        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
2925
2926        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
2927        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
2928
2929        self._poly_model.setData(npts_index, npts)
2930        self._poly_model.setData(nsigs_index, nsigs)
2931
2932        self.iterateOverModel(updateFunctionCaption)
2933        self.orig_poly_index = combo_box.currentIndex()
2934
2935    def loadPolydispArray(self, row_index):
2936        """
2937        Show the load file dialog and loads requested data into state
2938        """
2939        datafile = QtWidgets.QFileDialog.getOpenFileName(
2940            self, "Choose a weight file", "", "All files (*.*)", None,
2941            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
2942
2943        if not datafile:
2944            logger.info("No weight data chosen.")
2945            raise IOError
2946
2947        values = []
2948        weights = []
2949        def appendData(data_tuple):
2950            """
2951            Fish out floats from a tuple of strings
2952            """
2953            try:
2954                values.append(float(data_tuple[0]))
2955                weights.append(float(data_tuple[1]))
2956            except (ValueError, IndexError):
2957                # just pass through if line with bad data
2958                return
2959
2960        with open(datafile, 'r') as column_file:
2961            column_data = [line.rstrip().split() for line in column_file.readlines()]
2962            [appendData(line) for line in column_data]
2963
2964        # If everything went well - update the sasmodel values
2965        self.disp_model = POLYDISPERSITY_MODELS['array']()
2966        self.disp_model.set_weights(np.array(values), np.array(weights))
2967        # + update the cell with filename
2968        fname = os.path.basename(str(datafile))
2969        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2970        self._poly_model.setData(fname_index, fname)
2971
2972    def onColumnWidthUpdate(self, index, old_size, new_size):
2973        """
2974        Simple state update of the current column widths in the  param list
2975        """
2976        self.lstParamHeaderSizes[index] = new_size
2977
2978    def setMagneticModel(self):
2979        """
2980        Set magnetism values on model
2981        """
2982        if not self.model_parameters:
2983            return
2984        self._magnet_model.clear()
2985        # default initial value
2986        m0 = 0.5
2987        for param in self.model_parameters.call_parameters:
2988            if param.type != 'magnetic': continue
2989            if "M0" in param.name:
2990                m0 += 0.5
2991                value = m0
2992            else:
2993                value = param.default
2994            self.addCheckedMagneticListToModel(param, value)
2995
2996        FittingUtilities.addHeadersToModel(self._magnet_model)
2997
2998    def shellNamesList(self):
2999        """
3000        Returns list of names of all multi-shell parameters
3001        E.g. for sld[n], radius[n], n=1..3 it will return
3002        [sld1, sld2, sld3, radius1, radius2, radius3]
3003        """
3004        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
3005        top_index = self.kernel_module.multiplicity_info.number
3006        shell_names = []
3007        for i in range(1, top_index+1):
3008            for name in multi_names:
3009                shell_names.append(name+str(i))
3010        return shell_names
3011
3012    def addCheckedMagneticListToModel(self, param, value):
3013        """
3014        Wrapper for model update with a subset of magnetic parameters
3015        """
3016        try:
3017            basename, _ = param.name.rsplit('_', 1)
3018        except ValueError:
3019            basename = param.name
3020        if basename in self.shell_names:
3021            try:
3022                shell_index = int(basename[-2:])
3023            except ValueError:
3024                shell_index = int(basename[-1:])
3025
3026            if shell_index > self.current_shell_displayed:
3027                return
3028
3029        checked_list = [param.name,
3030                        str(value),
3031                        str(param.limits[0]),
3032                        str(param.limits[1]),
3033                        param.units]
3034
3035        self.magnet_params[param.name] = value
3036
3037        FittingUtilities.addCheckedListToModel(self._magnet_model, checked_list)
3038
3039    def enableStructureFactorControl(self, structure_factor):
3040        """
3041        Add structure factors to the list of parameters
3042        """
3043        if self.kernel_module.is_form_factor or structure_factor == 'None':
3044            self.enableStructureCombo()
3045        else:
3046            self.disableStructureCombo()
3047
3048    def addExtraShells(self):
3049        """
3050        Add a combobox for multiple shell display
3051        """
3052        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
3053
3054        if param_length == 0:
3055            return
3056
3057        # cell 1: variable name
3058        item1 = QtGui.QStandardItem(param_name)
3059
3060        func = QtWidgets.QComboBox()
3061
3062        # cell 2: combobox
3063        item2 = QtGui.QStandardItem()
3064
3065        # cell 3: min value
3066        item3 = QtGui.QStandardItem()
3067
3068        # cell 4: max value
3069        item4 = QtGui.QStandardItem()
3070
3071        # cell 4: SLD button
3072        item5 = QtGui.QStandardItem()
3073        button = QtWidgets.QPushButton()
3074        button.setText("Show SLD Profile")
3075
3076        self._model_model.appendRow([item1, item2, item3, item4, item5])
3077
3078        # Beautify the row:  span columns 2-4
3079        shell_row = self._model_model.rowCount()
3080        shell_index = self._model_model.index(shell_row-1, 1)
3081        button_index = self._model_model.index(shell_row-1, 4)
3082
3083        self.lstParams.setIndexWidget(shell_index, func)
3084        self.lstParams.setIndexWidget(button_index, button)
3085        self._n_shells_row = shell_row - 1
3086
3087        # Get the default number of shells for the model
3088        kernel_pars = self.kernel_module._model_info.parameters.kernel_parameters
3089        shell_par = None
3090        for par in kernel_pars:
3091            if par.name == param_name:
3092                shell_par = par
3093                break
3094        if not shell_par:
3095            logger.error("Could not find %s in kernel parameters.", param_name)
3096        default_shell_count = shell_par.default
3097        shell_min = 0
3098        shell_max = 0
3099        try:
3100            shell_min = int(shell_par.limits[0])
3101            shell_max = int(shell_par.limits[1])
3102        except IndexError as ex:
3103            # no info about limits
3104            pass
3105        # don't update the kernel here - this data is display only
3106        self._model_model.blockSignals(True)
3107        item3.setText(str(shell_min))
3108        item4.setText(str(shell_max))
3109        self._model_model.blockSignals(False)
3110
3111        # Respond to index change
3112        func.currentTextChanged.connect(self.modifyShellsInList)
3113
3114        # Respond to button press
3115        button.clicked.connect(self.onShowSLDProfile)
3116
3117        # Available range of shells displayed in the combobox
3118        func.addItems([str(i) for i in range(shell_min, shell_max+1)])
3119
3120        # Add default number of shells to the model
3121        func.setCurrentText(str(default_shell_count))
3122
3123    def modifyShellsInList(self, text):
3124        """
3125        Add/remove additional multishell parameters
3126        """
3127        # Find row location of the combobox
3128        first_row = self._n_shells_row + 1
3129        remove_rows = self._num_shell_params
3130        try:
3131            index = int(text)
3132        except ValueError:
3133            # bad text on the control!
3134            index = 0
3135            logger.error("Multiplicity incorrect! Setting to 0")
3136        self.kernel_module.multiplicity = index
3137        if remove_rows > 1:
3138            self._model_model.removeRows(first_row, remove_rows)
3139
3140        new_rows = FittingUtilities.addShellsToModel(
3141                self.model_parameters,
3142                self._model_model,
3143                index,
3144                first_row,
3145                self.lstParams)
3146
3147        self._num_shell_params = len(new_rows)
3148        self.current_shell_displayed = index
3149
3150        # Param values for existing shells were reset to default; force all changes into kernel module
3151        for row in new_rows:
3152            par = row[0].text()
3153            val = GuiUtils.toDouble(row[1].text())
3154            self.kernel_module.setParam(par, val)
3155
3156        # Change 'n' in the parameter model; also causes recalculation
3157        self._model_model.item(self._n_shells_row, 1).setText(str(index))
3158
3159        # Update relevant models
3160        self.setPolyModel()
3161        if self.canHaveMagnetism():
3162            self.setMagneticModel()
3163
3164    def onShowSLDProfile(self):
3165        """
3166        Show a quick plot of SLD profile
3167        """
3168        # get profile data
3169        x, y = self.kernel_module.getProfile()
3170        y *= 1.0e6
3171        profile_data = Data1D(x=x, y=y)
3172        profile_data.name = "SLD"
3173        profile_data.scale = 'linear'
3174        profile_data.symbol = 'Line'
3175        profile_data.hide_error = True
3176        profile_data._xaxis = "R(\AA)"
3177        profile_data._yaxis = "SLD(10^{-6}\AA^{-2})"
3178
3179        plotter = PlotterWidget(self, quickplot=True)
3180        plotter.data = profile_data
3181        plotter.showLegend = True
3182        plotter.plot(hide_error=True, marker='-')
3183
3184        self.plot_widget = QtWidgets.QWidget()
3185        self.plot_widget.setWindowTitle("Scattering Length Density Profile")
3186        layout = QtWidgets.QVBoxLayout()
3187        layout.addWidget(plotter)
3188        self.plot_widget.setLayout(layout)
3189        self.plot_widget.show()
3190
3191    def setInteractiveElements(self, enabled=True):
3192        """
3193        Switch interactive GUI elements on/off
3194        """
3195        assert isinstance(enabled, bool)
3196
3197        self.lstParams.setEnabled(enabled)
3198        self.lstPoly.setEnabled(enabled)
3199        self.lstMagnetic.setEnabled(enabled)
3200
3201        self.cbCategory.setEnabled(enabled)
3202        self.cbModel.setEnabled(enabled)
3203        self.cmdPlot.setEnabled(enabled)
3204
3205    def enableInteractiveElements(self):
3206        """
3207        Set buttion caption on fitting/calculate finish
3208        Enable the param table(s)
3209        """
3210        # Notify the user that fitting is available
3211        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
3212        self.cmdFit.setText("Fit")
3213        self.fit_started = False
3214        self.setInteractiveElements(True)
3215
3216    def disableInteractiveElements(self):
3217        """
3218        Set buttion caption on fitting/calculate start
3219        Disable the param table(s)
3220        """
3221        # Notify the user that fitting is being run
3222        # Allow for stopping the job
3223        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
3224        self.cmdFit.setText('Stop fit')
3225        self.setInteractiveElements(False)
3226
3227    def readFitPage(self, fp):
3228        """
3229        Read in state from a fitpage object and update GUI
3230        """
3231        assert isinstance(fp, FitPage)
3232        # Main tab info
3233        self.logic.data.filename = fp.filename
3234        self.data_is_loaded = fp.data_is_loaded
3235        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
3236        self.chkMagnetism.setCheckState(fp.is_magnetic)
3237        self.chk2DView.setCheckState(fp.is2D)
3238
3239        # Update the comboboxes
3240        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
3241        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
3242        if fp.current_factor:
3243            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
3244
3245        self.chi2 = fp.chi2
3246
3247        # Options tab
3248        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
3249        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
3250        self.npts = fp.fit_options[fp.NPTS]
3251        self.log_points = fp.fit_options[fp.LOG_POINTS]
3252        self.weighting = fp.fit_options[fp.WEIGHTING]
3253
3254        # Models
3255        self._model_model = fp.model_model
3256        self._poly_model = fp.poly_model
3257        self._magnet_model = fp.magnetism_model
3258
3259        # Resolution tab
3260        smearing = fp.smearing_options[fp.SMEARING_OPTION]
3261        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
3262        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
3263        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
3264        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
3265
3266        # TODO: add polidyspersity and magnetism
3267
3268    def saveToFitPage(self, fp):
3269        """
3270        Write current state to the given fitpage
3271        """
3272        assert isinstance(fp, FitPage)
3273
3274        # Main tab info
3275        fp.filename = self.logic.data.filename
3276        fp.data_is_loaded = self.data_is_loaded
3277        fp.is_polydisperse = self.chkPolydispersity.isChecked()
3278        fp.is_magnetic = self.chkMagnetism.isChecked()
3279        fp.is2D = self.chk2DView.isChecked()
3280        fp.data = self.data
3281
3282        # Use current models - they contain all the required parameters
3283        fp.model_model = self._model_model
3284        fp.poly_model = self._poly_model
3285        fp.magnetism_model = self._magnet_model
3286
3287        if self.cbCategory.currentIndex() != 0:
3288            fp.current_category = str(self.cbCategory.currentText())
3289            fp.current_model = str(self.cbModel.currentText())
3290
3291        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
3292            fp.current_factor = str(self.cbStructureFactor.currentText())
3293        else:
3294            fp.current_factor = ''
3295
3296        fp.chi2 = self.chi2
3297        fp.main_params_to_fit = self.main_params_to_fit
3298        fp.poly_params_to_fit = self.poly_params_to_fit
3299        fp.magnet_params_to_fit = self.magnet_params_to_fit
3300        fp.kernel_module = self.kernel_module
3301
3302        # Algorithm options
3303        # fp.algorithm = self.parent.fit_options.selected_id
3304
3305        # Options tab
3306        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
3307        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
3308        fp.fit_options[fp.NPTS] = self.npts
3309        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
3310        fp.fit_options[fp.LOG_POINTS] = self.log_points
3311        fp.fit_options[fp.WEIGHTING] = self.weighting
3312
3313        # Resolution tab
3314        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3315        fp.smearing_options[fp.SMEARING_OPTION] = smearing
3316        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
3317        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
3318        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
3319
3320        # TODO: add polidyspersity and magnetism
3321
3322    def updateUndo(self):
3323        """
3324        Create a new state page and add it to the stack
3325        """
3326        if self.undo_supported:
3327            self.pushFitPage(self.currentState())
3328
3329    def currentState(self):
3330        """
3331        Return fit page with current state
3332        """
3333        new_page = FitPage()
3334        self.saveToFitPage(new_page)
3335
3336        return new_page
3337
3338    def pushFitPage(self, new_page):
3339        """
3340        Add a new fit page object with current state
3341        """
3342        self.page_stack.append(new_page)
3343
3344    def popFitPage(self):
3345        """
3346        Remove top fit page from stack
3347        """
3348        if self.page_stack:
3349            self.page_stack.pop()
3350
3351    def getReport(self):
3352        """
3353        Create and return HTML report with parameters and charts
3354        """
3355        index = None
3356        if self.all_data:
3357            index = self.all_data[self.data_index]
3358        else:
3359            index = self.theory_item
3360        report_logic = ReportPageLogic(self,
3361                                       kernel_module=self.kernel_module,
3362                                       data=self.data,
3363                                       index=index,
3364                                       model=self._model_model)
3365
3366        return report_logic.reportList()
3367
3368    def savePageState(self):
3369        """
3370        Create and serialize local PageState
3371        """
3372        filepath = self.saveAsAnalysisFile()
3373        if filepath is None or filepath == "":
3374            return
3375
3376        fitpage_state = self.getFitPage()
3377        fitpage_state += self.getFitModel()
3378
3379        with open(filepath, 'w') as statefile:
3380            for line in fitpage_state:
3381                statefile.write(str(line))
3382
3383        self.communicate.statusBarUpdateSignal.emit('Analysis saved.')
3384
3385    def saveAsAnalysisFile(self):
3386        """
3387        Show the save as... dialog and return the chosen filepath
3388        """
3389        default_name = "FitPage"+str(self.tab_id)+".fitv"
3390
3391        wildcard = "fitv files (*.fitv)"
3392        kwargs = {
3393            'caption'   : 'Save As',
3394            'directory' : default_name,
3395            'filter'    : wildcard,
3396            'parent'    : None,
3397        }
3398        # Query user for filename.
3399        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
3400        filename = filename_tuple[0]
3401        return filename
3402
3403    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
3404        """
3405        This is a callback method called from the CANSAS reader.
3406        We need the instance of this reader only for writing out a file,
3407        so there's nothing here.
3408        Until Load Analysis is implemented, that is.
3409        """
3410        pass
3411
3412    def loadPageState(self, pagestate=None):
3413        """
3414        Load the PageState object and update the current widget
3415        """
3416        filepath = self.loadAnalysisFile()
3417        if filepath is None or filepath == "":
3418            return
3419
3420        with open(filepath, 'r') as statefile:
3421            #column_data = [line.rstrip().split() for line in statefile.readlines()]
3422            lines = statefile.readlines()
3423
3424        # convert into list of lists
3425        pass
3426
3427    def loadAnalysisFile(self):
3428        """
3429        Called when the "Open Project" menu item chosen.
3430        """
3431        default_name = "FitPage"+str(self.tab_id)+".fitv"
3432        wildcard = "fitv files (*.fitv)"
3433        kwargs = {
3434            'caption'   : 'Open Analysis',
3435            'directory' : default_name,
3436            'filter'    : wildcard,
3437            'parent'    : self,
3438        }
3439        filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
3440        return filename
3441
3442    def onCopyToClipboard(self, format=None):
3443        """
3444        Copy current fitting parameters into the clipboard
3445        using requested formatting:
3446        plain, excel, latex
3447        """
3448        param_list = self.getFitParameters()
3449        if format=="":
3450            param_list = self.getFitPage()
3451            param_list += self.getFitModel()
3452            formatted_output = FittingUtilities.formatParameters(param_list)
3453        elif format == "Excel":
3454            formatted_output = FittingUtilities.formatParametersExcel(param_list[1:])
3455        elif format == "Latex":
3456            formatted_output = FittingUtilities.formatParametersLatex(param_list[1:])
3457        else:
3458            raise AttributeError("Bad parameter output format specifier.")
3459
3460        # Dump formatted_output to the clipboard
3461        cb = QtWidgets.QApplication.clipboard()
3462        cb.setText(formatted_output)
3463
3464    def getFitModel(self):
3465        """
3466        serializes combobox state
3467        """
3468        param_list = []
3469        model = str(self.cbModel.currentText())
3470        category = str(self.cbCategory.currentText())
3471        structure = str(self.cbStructureFactor.currentText())
3472        param_list.append(['fitpage_category', category])
3473        param_list.append(['fitpage_model', model])
3474        param_list.append(['fitpage_structure', structure])
3475
3476        return param_list
3477
3478    def getFitPage(self):
3479        """
3480        serializes full state of this fit page
3481        """
3482        # run a loop over all parameters and pull out
3483        # first - regular params
3484        param_list = self.getFitParameters()
3485
3486        param_list.append(['is_data', str(self.data_is_loaded)])
3487        if self.data_is_loaded:
3488            param_list.append(['data_id', str(self.logic.data.id)])
3489            param_list.append(['data_name', str(self.logic.data.filename)])
3490
3491        # option tab
3492        param_list.append(['q_range_min', str(self.q_range_min)])
3493        param_list.append(['q_range_max', str(self.q_range_max)])
3494        param_list.append(['q_weighting', str(self.weighting)])
3495        param_list.append(['weighting', str(self.options_widget.weighting)])
3496
3497        # resolution
3498        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3499        index = self.smearing_widget.cbSmearing.currentIndex()
3500        param_list.append(['smearing', str(index)])
3501        param_list.append(['smearing_min', str(smearing_min)])
3502        param_list.append(['smearing_max', str(smearing_max)])
3503
3504        # checkboxes, if required
3505        has_polydisp = self.chkPolydispersity.isChecked()
3506        has_magnetism = self.chkMagnetism.isChecked()
3507        has_chain = self.chkChainFit.isChecked()
3508        has_2D = self.chk2DView.isChecked()
3509        param_list.append(['polydisperse_params', str(has_polydisp)])
3510        param_list.append(['magnetic_params', str(has_magnetism)])
3511        param_list.append(['chainfit_params', str(has_chain)])
3512        param_list.append(['2D_params', str(has_2D)])
3513
3514        return param_list
3515
3516    def getFitParameters(self):
3517        """
3518        serializes current parameters
3519        """
3520        param_list = []
3521        param_list.append(['model_name', str(self.cbModel.currentText())])
3522
3523        def gatherParams(row):
3524            """
3525            Create list of main parameters based on _model_model
3526            """
3527            param_name = str(self._model_model.item(row, 0).text())
3528
3529            # Assure this is a parameter - must contain a checkbox
3530            if not self._model_model.item(row, 0).isCheckable():
3531                # maybe it is a combobox item (multiplicity)
3532                try:
3533                    index = self._model_model.index(row, 1)
3534                    widget = self.lstParams.indexWidget(index)
3535                    if widget is None:
3536                        return
3537                    if isinstance(widget, QtWidgets.QComboBox):
3538                        # find the index of the combobox
3539                        current_index = widget.currentIndex()
3540                        param_list.append([param_name, 'None', str(current_index)])
3541                except Exception as ex:
3542                    pass
3543                return
3544
3545            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3546            # Value of the parameter. In some cases this is the text of the combobox choice.
3547            param_value = str(self._model_model.item(row, 1).text())
3548            param_error = None
3549            param_min = None
3550            param_max = None
3551            column_offset = 0
3552            if self.has_error_column:
3553                column_offset = 1
3554                param_error = str(self._model_model.item(row, 1+column_offset).text())
3555            try:
3556                param_min = str(self._model_model.item(row, 2+column_offset).text())
3557                param_max = str(self._model_model.item(row, 3+column_offset).text())
3558            except:
3559                pass
3560
3561            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3562
3563        def gatherPolyParams(row):
3564            """
3565            Create list of polydisperse parameters based on _poly_model
3566            """
3567            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
3568            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3569            param_value = str(self._poly_model.item(row, 1).text())
3570            param_error = None
3571            column_offset = 0
3572            if self.has_poly_error_column:
3573                column_offset = 1
3574                param_error = str(self._poly_model.item(row, 1+column_offset).text())
3575            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
3576            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
3577            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
3578            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
3579            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
3580            index = self._poly_model.index(row, 6+column_offset)
3581            widget = self.lstPoly.indexWidget(index)
3582            if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3583                param_fun = widget.currentText()
3584            # width
3585            name = param_name+".width"
3586            param_list.append([name, param_checked, param_value, param_error,
3587                               param_min, param_max, param_npts, param_nsigs, param_fun])
3588
3589        def gatherMagnetParams(row):
3590            """
3591            Create list of magnetic parameters based on _magnet_model
3592            """
3593            param_name = str(self._magnet_model.item(row, 0).text())
3594            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3595            param_value = str(self._magnet_model.item(row, 1).text())
3596            param_error = None
3597            column_offset = 0
3598            if self.has_magnet_error_column:
3599                column_offset = 1
3600                param_error = str(self._magnet_model.item(row, 1+column_offset).text())
3601            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
3602            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
3603            param_list.append([param_name, param_checked, param_value,
3604                               param_error, param_min, param_max])
3605
3606        self.iterateOverModel(gatherParams)
3607        if self.chkPolydispersity.isChecked():
3608            self.iterateOverPolyModel(gatherPolyParams)
3609        if self.chkMagnetism.isChecked() and self.canHaveMagnetism():
3610            self.iterateOverMagnetModel(gatherMagnetParams)
3611
3612        if self.kernel_module.is_multiplicity_model:
3613            param_list.append(['multiplicity', str(self.kernel_module.multiplicity)])
3614
3615        return param_list
3616
3617    def onParameterPaste(self):
3618        """
3619        Use the clipboard to update fit state
3620        """
3621        # Check if the clipboard contains right stuff
3622        cb = QtWidgets.QApplication.clipboard()
3623        cb_text = cb.text()
3624
3625        context = {}
3626        lines = cb_text.split(':')
3627        if lines[0] != 'sasview_parameter_values':
3628            return False
3629
3630        # put the text into dictionary
3631        line_dict = {}
3632        for line in lines[1:]:
3633            content = line.split(',')
3634            if len(content) > 1:
3635                line_dict[content[0]] = content[1:]
3636
3637        model = line_dict['model_name'][0]
3638
3639        if 'model_name' not in line_dict.keys():
3640            return False
3641
3642        if 'multiplicity' in line_dict.keys():
3643            multip = int(line_dict['multiplicity'][0], 0)
3644            # reset the model with multiplicity, so further updates are saved
3645            if self.kernel_module.is_multiplicity_model:
3646                self.kernel_module.multiplicity=multip
3647                self.updateMultiplicityCombo(multip)
3648
3649        if 'polydisperse_params' in line_dict.keys():
3650            self.chkPolydispersity.setChecked(line_dict['polydisperse_params'][0]=='True')
3651        if 'magnetic_params' in line_dict.keys():
3652            self.chkMagnetism.setChecked(line_dict['magnetic_params'][0]=='True')
3653        if 'chainfit_params' in line_dict.keys():
3654            self.chkChainFit.setChecked(line_dict['chainfit_params'][0]=='True')
3655        if '2D_params' in line_dict.keys():
3656            self.chk2DView.setChecked(line_dict['2D_params'][0]=='True')
3657
3658        # Create the context dictionary for parameters
3659        context['model_name'] = model
3660        for key, value in line_dict.items():
3661            if len(value) > 2:
3662                context[key] = value
3663
3664        if str(self.cbModel.currentText()) != str(context['model_name']):
3665            msg = QtWidgets.QMessageBox()
3666            msg.setIcon(QtWidgets.QMessageBox.Information)
3667            msg.setText("The model in the clipboard is not the same as the currently loaded model. \
3668                         Not all parameters saved may paste correctly.")
3669            msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
3670            result = msg.exec_()
3671            if result == QtWidgets.QMessageBox.Ok:
3672                pass
3673            else:
3674                return
3675
3676        if 'smearing' in line_dict.keys():
3677            try:
3678                index = int(line_dict['smearing'][0])
3679                self.smearing_widget.cbSmearing.setCurrentIndex(index)
3680            except ValueError:
3681                pass
3682        if 'smearing_min' in line_dict.keys():
3683            try:
3684                self.smearing_widget.dq_l = float(line_dict['smearing_min'][0])
3685            except ValueError:
3686                pass
3687        if 'smearing_max' in line_dict.keys():
3688            try:
3689                self.smearing_widget.dq_r = float(line_dict['smearing_max'][0])
3690            except ValueError:
3691                pass
3692
3693        if 'q_range_max' in line_dict.keys():
3694            try:
3695                self.q_range_min = float(line_dict['q_range_min'][0])
3696                self.q_range_max = float(line_dict['q_range_max'][0])
3697            except ValueError:
3698                pass
3699        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
3700        try:
3701            button_id = int(line_dict['weighting'][0])
3702            for button in self.options_widget.weightingGroup.buttons():
3703                if abs(self.options_widget.weightingGroup.id(button)) == button_id+2:
3704                    button.setChecked(True)
3705                    break
3706        except ValueError:
3707            pass
3708
3709        self.updateFullModel(context)
3710        self.updateFullPolyModel(context)
3711        self.updateFullMagnetModel(context)
3712
3713    def updateMultiplicityCombo(self, multip):
3714        """
3715        Find and update the multiplicity combobox
3716        """
3717        index = self._model_model.index(self._n_shells_row, 1)
3718        widget = self.lstParams.indexWidget(index)
3719        if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3720            widget.setCurrentIndex(widget.findText(str(multip)))
3721        self.current_shell_displayed = multip
3722
3723    def updateFullModel(self, param_dict):
3724        """
3725        Update the model with new parameters
3726        """
3727        assert isinstance(param_dict, dict)
3728        if not dict:
3729            return
3730
3731        def updateFittedValues(row):
3732            # Utility function for main model update
3733            # internal so can use closure for param_dict
3734            param_name = str(self._model_model.item(row, 0).text())
3735            if param_name not in list(param_dict.keys()):
3736                return
3737            # Special case of combo box in the cell (multiplicity)
3738            param_line = param_dict[param_name]
3739            if len(param_line) == 1:
3740                # modify the shells value
3741                try:
3742                    combo_index = int(param_line[0])
3743                except ValueError:
3744                    # quietly pass
3745                    return
3746                index = self._model_model.index(row, 1)
3747                widget = self.lstParams.indexWidget(index)
3748                if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3749                    #widget.setCurrentIndex(combo_index)
3750                    return
3751            # checkbox state
3752            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3753            self._model_model.item(row, 0).setCheckState(param_checked)
3754
3755            # parameter value can be either just a value or text on the combobox
3756            param_text = param_dict[param_name][1]
3757            index = self._model_model.index(row, 1)
3758            widget = self.lstParams.indexWidget(index)
3759            if widget is not None and isinstance(widget, QtWidgets.QComboBox):
3760                # Find the right index based on text
3761                combo_index = int(param_text, 0)
3762                widget.setCurrentIndex(combo_index)
3763            else:
3764                # modify the param value
3765                param_repr = GuiUtils.formatNumber(param_text, high=True)
3766                self._model_model.item(row, 1).setText(param_repr)
3767
3768            # Potentially the error column
3769            ioffset = 0
3770            joffset = 0
3771            if len(param_dict[param_name])>4:
3772                # error values are not editable - no need to update
3773                ioffset = 1
3774            if self.has_error_column:
3775                joffset = 1
3776            # min/max
3777            try:
3778                param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3779                self._model_model.item(row, 2+joffset).setText(param_repr)
3780                param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3781                self._model_model.item(row, 3+joffset).setText(param_repr)
3782            except:
3783                pass
3784
3785            self.setFocus()
3786
3787        self.iterateOverModel(updateFittedValues)
3788
3789    def updateFullPolyModel(self, param_dict):
3790        """
3791        Update the polydispersity model with new parameters, create the errors column
3792        """
3793        assert isinstance(param_dict, dict)
3794        if not dict:
3795            return
3796
3797        def updateFittedValues(row):
3798            # Utility function for main model update
3799            # internal so can use closure for param_dict
3800            if row >= self._poly_model.rowCount():
3801                return
3802            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
3803            if param_name not in list(param_dict.keys()):
3804                return
3805            # checkbox state
3806            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3807            self._poly_model.item(row,0).setCheckState(param_checked)
3808
3809            # modify the param value
3810            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3811            self._poly_model.item(row, 1).setText(param_repr)
3812
3813            # Potentially the error column
3814            ioffset = 0
3815            joffset = 0
3816            if len(param_dict[param_name])>7:
3817                ioffset = 1
3818            if self.has_poly_error_column:
3819                joffset = 1
3820            # min
3821            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3822            self._poly_model.item(row, 2+joffset).setText(param_repr)
3823            # max
3824            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3825            self._poly_model.item(row, 3+joffset).setText(param_repr)
3826            # Npts
3827            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
3828            self._poly_model.item(row, 4+joffset).setText(param_repr)
3829            # Nsigs
3830            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3831            self._poly_model.item(row, 5+joffset).setText(param_repr)
3832
3833            self.setFocus()
3834
3835        self.iterateOverPolyModel(updateFittedValues)
3836
3837    def updateFullMagnetModel(self, param_dict):
3838        """
3839        Update the magnetism model with new parameters, create the errors column
3840        """
3841        assert isinstance(param_dict, dict)
3842        if not dict:
3843            return
3844
3845        def updateFittedValues(row):
3846            # Utility function for main model update
3847            # internal so can use closure for param_dict
3848            if row >= self._magnet_model.rowCount():
3849                return
3850            param_name = str(self._magnet_model.item(row, 0).text()).rsplit()[-1]
3851            if param_name not in list(param_dict.keys()):
3852                return
3853            # checkbox state
3854            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3855            self._magnet_model.item(row,0).setCheckState(param_checked)
3856
3857            # modify the param value
3858            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3859            self._magnet_model.item(row, 1).setText(param_repr)
3860
3861            # Potentially the error column
3862            ioffset = 0
3863            joffset = 0
3864            if len(param_dict[param_name])>4:
3865                ioffset = 1
3866            if self.has_magnet_error_column:
3867                joffset = 1
3868            # min
3869            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3870            self._magnet_model.item(row, 2+joffset).setText(param_repr)
3871            # max
3872            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3873            self._magnet_model.item(row, 3+joffset).setText(param_repr)
3874
3875        self.iterateOverMagnetModel(updateFittedValues)
3876
3877    def getCurrentFitState(self, state=None):
3878        """
3879        Store current state for fit_page
3880        """
3881        # save model option
3882        #if self.model is not None:
3883        #    self.disp_list = self.getDispParamList()
3884        #    state.disp_list = copy.deepcopy(self.disp_list)
3885        #    #state.model = self.model.clone()
3886
3887        # Comboboxes
3888        state.categorycombobox = self.cbCategory.currentText()
3889        state.formfactorcombobox = self.cbModel.currentText()
3890        if self.cbStructureFactor.isEnabled():
3891            state.structurecombobox = self.cbStructureFactor.currentText()
3892        state.tcChi = self.chi2
3893
3894        state.enable2D = self.is2D
3895
3896        #state.weights = copy.deepcopy(self.weights)
3897        # save data
3898        state.data = copy.deepcopy(self.data)
3899
3900        # save plotting range
3901        state.qmin = self.q_range_min
3902        state.qmax = self.q_range_max
3903        state.npts = self.npts
3904
3905        #    self.state.enable_disp = self.enable_disp.GetValue()
3906        #    self.state.disable_disp = self.disable_disp.GetValue()
3907
3908        #    self.state.enable_smearer = \
3909        #                        copy.deepcopy(self.enable_smearer.GetValue())
3910        #    self.state.disable_smearer = \
3911        #                        copy.deepcopy(self.disable_smearer.GetValue())
3912
3913        #self.state.pinhole_smearer = \
3914        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
3915        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
3916        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
3917        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
3918        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
3919        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
3920
3921        p = self.model_parameters
3922        # save checkbutton state and txtcrtl values
3923        state.parameters = FittingUtilities.getStandardParam(self._model_model)
3924        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
3925
3926        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
3927        #self._copy_parameters_state(self.parameters, self.state.parameters)
3928        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
3929        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
3930
Note: See TracBrowser for help on using the repository browser.