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

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

Working version of Save/Load? Analysis. SASVIEW-983.
Changed the default behaviour of Category/Model? combos:
Selecting a category does not pre-select the first model now.

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