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

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 7fd20fc was 7fd20fc, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Initial commit for constrained & simultaneous fitting functionality SASVIEW-277

  • Property mode set to 100644
File size: 76.9 KB
Line 
1import json
2import os
3from collections import defaultdict
4
5
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 product
17from sasmodels import generate
18from sasmodels import modelinfo
19from sasmodels.sasview_model import load_standard_models
20from sasmodels.weights import MODELS as POLYDISPERSITY_MODELS
21
22from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
23
24import sas.qtgui.Utilities.GuiUtils as GuiUtils
25from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
26from sas.qtgui.Plotting.PlotterData import Data1D
27from sas.qtgui.Plotting.PlotterData import Data2D
28
29from sas.qtgui.Perspectives.Fitting.UI.FittingWidgetUI import Ui_FittingWidgetUI
30from sas.qtgui.Perspectives.Fitting.FitThread import FitThread
31from sas.qtgui.Perspectives.Fitting.ConsoleUpdate import ConsoleUpdate
32
33from sas.qtgui.Perspectives.Fitting.ModelThread import Calc1D
34from sas.qtgui.Perspectives.Fitting.ModelThread import Calc2D
35from sas.qtgui.Perspectives.Fitting.FittingLogic import FittingLogic
36from sas.qtgui.Perspectives.Fitting import FittingUtilities
37from sas.qtgui.Perspectives.Fitting.SmearingWidget import SmearingWidget
38from sas.qtgui.Perspectives.Fitting.OptionsWidget import OptionsWidget
39from sas.qtgui.Perspectives.Fitting.FitPage import FitPage
40from sas.qtgui.Perspectives.Fitting.ViewDelegate import ModelViewDelegate
41from sas.qtgui.Perspectives.Fitting.ViewDelegate import PolyViewDelegate
42from sas.qtgui.Perspectives.Fitting.ViewDelegate import MagnetismViewDelegate
43
44
45TAB_MAGNETISM = 4
46TAB_POLY = 3
47CATEGORY_DEFAULT = "Choose category..."
48CATEGORY_STRUCTURE = "Structure Factor"
49STRUCTURE_DEFAULT = "None"
50
51DEFAULT_POLYDISP_FUNCTION = 'gaussian'
52
53USING_TWISTED = True
54#USING_TWISTED = False
55
56class ToolTippedItemModel(QtGui.QStandardItemModel):
57    """
58    Subclass from QStandardItemModel to allow displaying tooltips in
59    QTableView model.
60    """
61    def __init__(self, parent=None):
62        QtGui.QStandardItemModel.__init__(self,parent)
63
64    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
65        """
66        Displays tooltip for each column's header
67        :param section:
68        :param orientation:
69        :param role:
70        :return:
71        """
72        if role == QtCore.Qt.ToolTipRole:
73            if orientation == QtCore.Qt.Horizontal:
74                return str(self.header_tooltips[section])
75
76        return QtGui.QStandardItemModel.headerData(self, section, orientation, role)
77
78class FittingWidget(QtWidgets.QWidget, Ui_FittingWidgetUI):
79    """
80    Main widget for selecting form and structure factor models
81    """
82    def __init__(self, parent=None, data=None, tab_id=1):
83
84        super(FittingWidget, self).__init__()
85
86        # Necessary globals
87        self.parent = parent
88
89        # Which tab is this widget displayed in?
90        self.tab_id = tab_id
91
92        # Main Data[12]D holder
93        self.logic = FittingLogic()
94
95        # Globals
96        self.initializeGlobals()
97
98        # Main GUI setup up
99        self.setupUi(self)
100        self.setWindowTitle("Fitting")
101
102        # Set up tabs widgets
103        self.initializeWidgets()
104
105        # Set up models and views
106        self.initializeModels()
107
108        # Defaults for the structure factors
109        self.setDefaultStructureCombo()
110
111        # Make structure factor and model CBs disabled
112        self.disableModelCombo()
113        self.disableStructureCombo()
114
115        # Generate the category list for display
116        self.initializeCategoryCombo()
117
118        # Connect signals to controls
119        self.initializeSignals()
120
121        # Initial control state
122        self.initializeControls()
123
124        # Display HTML content
125        #self.setupHelp()
126
127        # New font to display angstrom symbol
128        new_font = 'font-family: -apple-system, "Helvetica Neue", "Ubuntu";'
129        self.label_17.setStyleSheet(new_font)
130        self.label_19.setStyleSheet(new_font)
131
132        self._index = None
133        if data is not None:
134            self.data = data
135
136    @property
137    def data(self):
138        return self.logic.data
139
140    @data.setter
141    def data(self, value):
142        """ data setter """
143        # Value is either a list of indices for batch fitting or a simple index
144        # for standard fitting. Assure we have a list, regardless.
145        if isinstance(value, list):
146            self.is_batch_fitting = True
147        else:
148            value = [value]
149
150        assert isinstance(value[0], QtGui.QStandardItem)
151        # _index contains the QIndex with data
152        self._index = value[0]
153
154        # Keep reference to all datasets for batch
155        self.all_data = value
156
157        # Update logics with data items
158        # Logics.data contains only a single Data1D/Data2D object
159        self.logic.data = GuiUtils.dataFromItem(value[0])
160
161        # Overwrite data type descriptor
162        self.is2D = True if isinstance(self.logic.data, Data2D) else False
163
164        # Let others know we're full of data now
165        self.data_is_loaded = True
166
167        # Enable/disable UI components
168        self.setEnablementOnDataLoad()
169
170    def initializeGlobals(self):
171        """
172        Initialize global variables used in this class
173        """
174        # SasModel is loaded
175        self.model_is_loaded = False
176        # Data[12]D passed and set
177        self.data_is_loaded = False
178        # Batch/single fitting
179        self.is_batch_fitting = False
180        self.is_chain_fitting = False
181        # Current SasModel in view
182        self.kernel_module = None
183        # Current SasModel view dimension
184        self.is2D = False
185        # Current SasModel is multishell
186        self.model_has_shells = False
187        # Utility variable to enable unselectable option in category combobox
188        self._previous_category_index = 0
189        # Utility variable for multishell display
190        self._last_model_row = 0
191        # Dictionary of {model name: model class} for the current category
192        self.models = {}
193        # Parameters to fit
194        self.parameters_to_fit = None
195        # Fit options
196        self.q_range_min = 0.005
197        self.q_range_max = 0.1
198        self.npts = 25
199        self.log_points = False
200        self.weighting = 0
201        self.chi2 = None
202        # Does the control support UNDO/REDO
203        # temporarily off
204        self.undo_supported = False
205        self.page_stack = []
206        self.all_data = []
207        # Polydisp widget table default index for function combobox
208        self.orig_poly_index = 3
209
210        # Data for chosen model
211        self.model_data = None
212
213        # Which shell is being currently displayed?
214        self.current_shell_displayed = 0
215        # List of all shell-unique parameters
216        self.shell_names = []
217
218        # Error column presence in parameter display
219        self.has_error_column = False
220        self.has_poly_error_column = False
221        self.has_magnet_error_column = False
222
223        # signal communicator
224        self.communicate = self.parent.communicate
225
226    def initializeWidgets(self):
227        """
228        Initialize widgets for tabs
229        """
230        # Options widget
231        layout = QtWidgets.QGridLayout()
232        self.options_widget = OptionsWidget(self, self.logic)
233        layout.addWidget(self.options_widget)
234        self.tabOptions.setLayout(layout)
235
236        # Smearing widget
237        layout = QtWidgets.QGridLayout()
238        self.smearing_widget = SmearingWidget(self)
239        layout.addWidget(self.smearing_widget)
240        self.tabResolution.setLayout(layout)
241
242        # Define bold font for use in various controls
243        self.boldFont = QtGui.QFont()
244        self.boldFont.setBold(True)
245
246        # Set data label
247        self.label.setFont(self.boldFont)
248        self.label.setText("No data loaded")
249        self.lblFilename.setText("")
250
251        # Magnetic angles explained in one picture
252        self.magneticAnglesWidget = QtWidgets.QWidget()
253        labl = QtWidgets.QLabel(self.magneticAnglesWidget)
254        pixmap = QtGui.QPixmap(GuiUtils.IMAGES_DIRECTORY_LOCATION + '/M_angles_pic.bmp')
255        labl.setPixmap(pixmap)
256        self.magneticAnglesWidget.setFixedSize(pixmap.width(), pixmap.height())
257
258    def initializeModels(self):
259        """
260        Set up models and views
261        """
262        # Set the main models
263        # We can't use a single model here, due to restrictions on flattening
264        # the model tree with subclassed QAbstractProxyModel...
265        self._model_model = ToolTippedItemModel()
266        self._poly_model = ToolTippedItemModel()
267        self._magnet_model = ToolTippedItemModel()
268
269        # Param model displayed in param list
270        self.lstParams.setModel(self._model_model)
271        self.readCategoryInfo()
272
273        self.model_parameters = None
274
275        # Delegates for custom editing and display
276        self.lstParams.setItemDelegate(ModelViewDelegate(self))
277
278        self.lstParams.setAlternatingRowColors(True)
279        stylesheet = """
280
281            QTreeView {
282                paint-alternating-row-colors-for-empty-area:0;
283            }
284
285            QTreeView::item:hover {
286                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1);
287                border: 1px solid #bfcde4;
288            }
289
290            QTreeView::item:selected {
291                border: 1px solid #567dbc;
292            }
293
294            QTreeView::item:selected:active{
295                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6ea1f1, stop: 1 #567dbc);
296            }
297
298            QTreeView::item:selected:!active {
299                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6b9be8, stop: 1 #577fbf);
300            }
301           """
302        self.lstParams.setStyleSheet(stylesheet)
303        self.lstParams.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
304        self.lstParams.customContextMenuRequested.connect(self.showModelContextMenu)
305        self.lstParams.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False)
306        # Poly model displayed in poly list
307        self.lstPoly.setModel(self._poly_model)
308        self.setPolyModel()
309        self.setTableProperties(self.lstPoly)
310        # Delegates for custom editing and display
311        self.lstPoly.setItemDelegate(PolyViewDelegate(self))
312        # Polydispersity function combo response
313        self.lstPoly.itemDelegate().combo_updated.connect(self.onPolyComboIndexChange)
314        self.lstPoly.itemDelegate().filename_updated.connect(self.onPolyFilenameChange)
315
316        # Magnetism model displayed in magnetism list
317        self.lstMagnetic.setModel(self._magnet_model)
318        self.setMagneticModel()
319        self.setTableProperties(self.lstMagnetic)
320        # Delegates for custom editing and display
321        self.lstMagnetic.setItemDelegate(MagnetismViewDelegate(self))
322
323    def initializeCategoryCombo(self):
324        """
325        Model category combo setup
326        """
327        category_list = sorted(self.master_category_dict.keys())
328        self.cbCategory.addItem(CATEGORY_DEFAULT)
329        self.cbCategory.addItems(category_list)
330        self.cbCategory.addItem(CATEGORY_STRUCTURE)
331        self.cbCategory.setCurrentIndex(0)
332
333    def setEnablementOnDataLoad(self):
334        """
335        Enable/disable various UI elements based on data loaded
336        """
337        # Tag along functionality
338        self.label.setText("Data loaded from: ")
339        self.lblFilename.setText(self.logic.data.filename)
340        self.updateQRange()
341        # Switch off Data2D control
342        self.chk2DView.setEnabled(False)
343        self.chk2DView.setVisible(False)
344        self.chkMagnetism.setEnabled(self.is2D)
345        self.tabFitting.setTabEnabled(TAB_MAGNETISM, self.is2D)
346        # Combo box or label for file name"
347        if self.is_batch_fitting:
348            self.lblFilename.setVisible(False)
349            for dataitem in self.all_data:
350                filename = GuiUtils.dataFromItem(dataitem).filename
351                self.cbFileNames.addItem(filename)
352            self.cbFileNames.setVisible(True)
353            self.chkChainFit.setEnabled(True)
354            self.chkChainFit.setVisible(True)
355            # This panel is not designed to view individual fits, so disable plotting
356            self.cmdPlot.setVisible(False)
357        # Similarly on other tabs
358        self.options_widget.setEnablementOnDataLoad()
359        self.onSelectModel()
360        # Smearing tab
361        self.smearing_widget.updateSmearing(self.data)
362
363    def acceptsData(self):
364        """ Tells the caller this widget can accept new dataset """
365        return not self.data_is_loaded
366
367    def disableModelCombo(self):
368        """ Disable the combobox """
369        self.cbModel.setEnabled(False)
370        self.lblModel.setEnabled(False)
371
372    def enableModelCombo(self):
373        """ Enable the combobox """
374        self.cbModel.setEnabled(True)
375        self.lblModel.setEnabled(True)
376
377    def disableStructureCombo(self):
378        """ Disable the combobox """
379        self.cbStructureFactor.setEnabled(False)
380        self.lblStructure.setEnabled(False)
381
382    def enableStructureCombo(self):
383        """ Enable the combobox """
384        self.cbStructureFactor.setEnabled(True)
385        self.lblStructure.setEnabled(True)
386
387    def togglePoly(self, isChecked):
388        """ Enable/disable the polydispersity tab """
389        self.tabFitting.setTabEnabled(TAB_POLY, isChecked)
390
391    def toggleMagnetism(self, isChecked):
392        """ Enable/disable the magnetism tab """
393        self.tabFitting.setTabEnabled(TAB_MAGNETISM, isChecked)
394
395    def toggleChainFit(self, isChecked):
396        """ Enable/disable chain fitting """
397        self.is_chain_fitting = isChecked
398
399    def toggle2D(self, isChecked):
400        """ Enable/disable the controls dependent on 1D/2D data instance """
401        self.chkMagnetism.setEnabled(isChecked)
402        self.is2D = isChecked
403        # Reload the current model
404        if self.kernel_module:
405            self.onSelectModel()
406
407    def initializeControls(self):
408        """
409        Set initial control enablement
410        """
411        self.cbFileNames.setVisible(False)
412        self.cmdFit.setEnabled(False)
413        self.cmdPlot.setEnabled(False)
414        self.options_widget.cmdComputePoints.setVisible(False) # probably redundant
415        self.chkPolydispersity.setEnabled(True)
416        self.chkPolydispersity.setCheckState(False)
417        self.chk2DView.setEnabled(True)
418        self.chk2DView.setCheckState(False)
419        self.chkMagnetism.setEnabled(False)
420        self.chkMagnetism.setCheckState(False)
421        self.chkChainFit.setEnabled(False)
422        self.chkChainFit.setVisible(False)
423        # Tabs
424        self.tabFitting.setTabEnabled(TAB_POLY, False)
425        self.tabFitting.setTabEnabled(TAB_MAGNETISM, False)
426        self.lblChi2Value.setText("---")
427        # Smearing tab
428        self.smearing_widget.updateSmearing(self.data)
429        # Line edits in the option tab
430        self.updateQRange()
431
432    def initializeSignals(self):
433        """
434        Connect GUI element signals
435        """
436        # Comboboxes
437        self.cbStructureFactor.currentIndexChanged.connect(self.onSelectStructureFactor)
438        self.cbCategory.currentIndexChanged.connect(self.onSelectCategory)
439        self.cbModel.currentIndexChanged.connect(self.onSelectModel)
440        self.cbFileNames.currentIndexChanged.connect(self.onSelectBatchFilename)
441        # Checkboxes
442        self.chk2DView.toggled.connect(self.toggle2D)
443        self.chkPolydispersity.toggled.connect(self.togglePoly)
444        self.chkMagnetism.toggled.connect(self.toggleMagnetism)
445        self.chkChainFit.toggled.connect(self.toggleChainFit)
446        # Buttons
447        self.cmdFit.clicked.connect(self.onFit)
448        self.cmdPlot.clicked.connect(self.onPlot)
449        self.cmdHelp.clicked.connect(self.onHelp)
450        self.cmdMagneticDisplay.clicked.connect(self.onDisplayMagneticAngles)
451
452        # Respond to change in parameters from the UI
453        self._model_model.itemChanged.connect(self.onMainParamsChange)
454        self._poly_model.itemChanged.connect(self.onPolyModelChange)
455        self._magnet_model.itemChanged.connect(self.onMagnetModelChange)
456
457        # Signals from separate tabs asking for replot
458        self.options_widget.plot_signal.connect(self.onOptionsUpdate)
459
460    def showModelContextMenu(self, position):
461
462        rows = len([s.row() for s in self.lstParams.selectionModel().selectedRows()])
463        menu = self.showModelDescription() if not rows else self.modelContextMenu(rows)
464        try:
465            menu.exec_(self.lstParams.viewport().mapToGlobal(position))
466        except AttributeError as ex:
467            logging.error("Error generating context menu: %s" % ex)
468        return
469
470    def modelContextMenu(self, rows):
471        menu = QtWidgets.QMenu()
472
473        # Select for fitting
474        self.actionSelect = QtWidgets.QAction(self)
475        self.actionSelect.setObjectName("actionSelect")
476        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select parameter for fitting"))
477        # Unselect from fitting
478        self.actionDeselect = QtWidgets.QAction(self)
479        self.actionDeselect.setObjectName("actionDeselect")
480        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select parameter from fitting"))
481
482        self.actionConstrain = QtWidgets.QAction(self)
483        self.actionConstrain.setObjectName("actionConstrain")
484        self.actionConstrain.setText(QtCore.QCoreApplication.translate("self", "Constrain parameter to current value"))
485
486        self.actionMultiConstrain = QtWidgets.QAction(self)
487        self.actionMultiConstrain.setObjectName("actionMultiConstrain")
488        self.actionMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Constrain selected parameters to their current values"))
489
490        self.actionMutualMultiConstrain = QtWidgets.QAction(self)
491        self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
492        self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of selected parameters..."))
493
494        #action.setDefaultWidget(label)
495        menu.addAction(self.actionSelect)
496        menu.addAction(self.actionDeselect)
497        menu.addSeparator()
498
499        if rows == 1:
500            menu.addAction(self.actionConstrain)
501        elif rows == 2:
502            menu.addAction(self.actionMultiConstrain)
503            menu.addAction(self.actionMutualMultiConstrain)
504        elif rows > 2:
505            menu.addAction(self.actionMultiConstrain)
506
507        # Define the callbacks
508        self.actionConstrain.triggered.connect(self.addConstraint)
509        self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstrain)
510        self.actionSelect.triggered.connect(self.selectParameters)
511        self.actionDeselect.triggered.connect(self.deselectParameters)
512        return menu
513
514    def showMultiConstrain(self):
515        """
516        Show the constraint widget and receive the expression
517        """
518        from sas.qtgui.Perspectives.Fitting.MultiConstraint import MultiConstraint
519        params_list = [s.data() for s in self.lstParams.selectionModel().selectedRows()]
520        mc_widget = MultiConstraint(self, params=params_list)
521        mc_widget.exec_()
522        constraint = mc_widget.txtConstraint.text()
523        # Pass the constraint to the parser
524        self.communicate.statusBarUpdateSignal.emit('Constraints added')
525        pass
526
527    def addConstraint(self):
528        """
529        Adds a constraint on a single parameter.
530        """
531        self.communicate.statusBarUpdateSignal.emit('Constraint added')
532        pass
533
534    def selectParameters(self):
535        """
536        Selected parameters are chosen for fitting
537        """
538        status = QtCore.Qt.Checked
539        self.setParameterSelection(status)
540
541    def deselectParameters(self):
542        """
543        Selected parameters are removed for fitting
544        """
545        status = QtCore.Qt.Unchecked
546        self.setParameterSelection(status)
547
548    def selectedParameters(self):
549        """ Returns list of selected (highlighted) parameters """
550        return [s.row() for s in self.lstParams.selectionModel().selectedRows() if self.isCheckable(s.row())]
551
552    def setParameterSelection(self, status=QtCore.Qt.Unchecked):
553        """
554        Selected parameters are chosen for fitting
555        """
556        # Convert to proper indices and set requested enablement
557        for row in self.selectedParameters():
558            self._model_model.item(row, 0).setCheckState(status)
559        pass
560
561    def showModelDescription(self):
562        """
563        Creates a window with model description, when right clicked in the treeview
564        """
565        msg = 'Model description:\n'
566        if self.kernel_module is not None:
567            if str(self.kernel_module.description).rstrip().lstrip() == '':
568                msg += "Sorry, no information is available for this model."
569            else:
570                msg += self.kernel_module.description + '\n'
571        else:
572            msg += "You must select a model to get information on this"
573
574        menu = QtWidgets.QMenu()
575        label = QtWidgets.QLabel(msg)
576        action = QtWidgets.QWidgetAction(self)
577        action.setDefaultWidget(label)
578        menu.addAction(action)
579        return menu
580
581    def onSelectModel(self):
582        """
583        Respond to select Model from list event
584        """
585        model = self.cbModel.currentText()
586
587        # empty combobox forced to be read
588        if not model:
589            return
590        # Reset structure factor
591        self.cbStructureFactor.setCurrentIndex(0)
592
593        # Reset parameters to fit
594        self.parameters_to_fit = None
595        self.has_error_column = False
596        self.has_poly_error_column = False
597
598        self.respondToModelStructure(model=model, structure_factor=None)
599
600    def onSelectBatchFilename(self, data_index):
601        """
602        Update the logic based on the selected file in batch fitting
603        """
604        self._index = self.all_data[data_index]
605        self.logic.data = GuiUtils.dataFromItem(self.all_data[data_index])
606        self.updateQRange()
607
608    def onSelectStructureFactor(self):
609        """
610        Select Structure Factor from list
611        """
612        model = str(self.cbModel.currentText())
613        category = str(self.cbCategory.currentText())
614        structure = str(self.cbStructureFactor.currentText())
615        if category == CATEGORY_STRUCTURE:
616            model = None
617        self.respondToModelStructure(model=model, structure_factor=structure)
618
619    def respondToModelStructure(self, model=None, structure_factor=None):
620        # Set enablement on calculate/plot
621        self.cmdPlot.setEnabled(True)
622
623        # kernel parameters -> model_model
624        self.SASModelToQModel(model, structure_factor)
625
626        if self.data_is_loaded:
627            self.cmdPlot.setText("Show Plot")
628            self.calculateQGridForModel()
629        else:
630            self.cmdPlot.setText("Calculate")
631            # Create default datasets if no data passed
632            self.createDefaultDataset()
633
634        # Update state stack
635        self.updateUndo()
636
637    def onSelectCategory(self):
638        """
639        Select Category from list
640        """
641        category = self.cbCategory.currentText()
642        # Check if the user chose "Choose category entry"
643        if category == CATEGORY_DEFAULT:
644            # if the previous category was not the default, keep it.
645            # Otherwise, just return
646            if self._previous_category_index != 0:
647                # We need to block signals, or else state changes on perceived unchanged conditions
648                self.cbCategory.blockSignals(True)
649                self.cbCategory.setCurrentIndex(self._previous_category_index)
650                self.cbCategory.blockSignals(False)
651            return
652
653        if category == CATEGORY_STRUCTURE:
654            self.disableModelCombo()
655            self.enableStructureCombo()
656            self._model_model.clear()
657            return
658
659        # Safely clear and enable the model combo
660        self.cbModel.blockSignals(True)
661        self.cbModel.clear()
662        self.cbModel.blockSignals(False)
663        self.enableModelCombo()
664        self.disableStructureCombo()
665
666        self._previous_category_index = self.cbCategory.currentIndex()
667        # Retrieve the list of models
668        model_list = self.master_category_dict[category]
669        # Populate the models combobox
670        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
671
672    def onPolyModelChange(self, item):
673        """
674        Callback method for updating the main model and sasmodel
675        parameters with the GUI values in the polydispersity view
676        """
677        model_column = item.column()
678        model_row = item.row()
679        name_index = self._poly_model.index(model_row, 0)
680        parameter_name = str(name_index.data()).lower() # "distribution of sld" etc.
681        if "distribution of" in parameter_name:
682            # just the last word
683            parameter_name = parameter_name.rsplit()[-1]
684
685        # Extract changed value.
686        if model_column == self.lstPoly.itemDelegate().poly_parameter:
687            # Is the parameter checked for fitting?
688            value = item.checkState()
689            parameter_name = parameter_name + '.width'
690            if value == QtCore.Qt.Checked:
691                self.parameters_to_fit.append(parameter_name)
692            else:
693                if parameter_name in self.parameters_to_fit:
694                    self.parameters_to_fit.remove(parameter_name)
695            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
696            return
697        elif model_column in [self.lstPoly.itemDelegate().poly_min, self.lstPoly.itemDelegate().poly_max]:
698            try:
699                value = GuiUtils.toDouble(item.text())
700            except TypeError:
701                # Can't be converted properly, bring back the old value and exit
702                return
703
704            current_details = self.kernel_module.details[parameter_name]
705            current_details[model_column-1] = value
706        elif model_column == self.lstPoly.itemDelegate().poly_function:
707            # name of the function - just pass
708            return
709        elif model_column == self.lstPoly.itemDelegate().poly_filename:
710            # filename for array - just pass
711            return
712        else:
713            try:
714                value = GuiUtils.toDouble(item.text())
715            except TypeError:
716                # Can't be converted properly, bring back the old value and exit
717                return
718
719            # Update the sasmodel
720            # PD[ratio] -> width, npts -> npts, nsigs -> nsigmas
721            self.kernel_module.setParam(parameter_name + '.' + \
722                                        self.lstPoly.itemDelegate().columnDict()[model_column], value)
723
724    def onMagnetModelChange(self, item):
725        """
726        Callback method for updating the sasmodel magnetic parameters with the GUI values
727        """
728        model_column = item.column()
729        model_row = item.row()
730        name_index = self._magnet_model.index(model_row, 0)
731        parameter_name = str(self._magnet_model.data(name_index))
732
733        if model_column == 0:
734            value = item.checkState()
735            if value == QtCore.Qt.Checked:
736                self.parameters_to_fit.append(parameter_name)
737            else:
738                if parameter_name in self.parameters_to_fit:
739                    self.parameters_to_fit.remove(parameter_name)
740            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
741            # Update state stack
742            self.updateUndo()
743            return
744
745        # Extract changed value.
746        try:
747            value = GuiUtils.toDouble(item.text())
748        except TypeError:
749            # Unparsable field
750            return
751
752        property_index = self._magnet_model.headerData(1, model_column)-1 # Value, min, max, etc.
753
754        # Update the parameter value - note: this supports +/-inf as well
755        self.kernel_module.params[parameter_name] = value
756
757        # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
758        self.kernel_module.details[parameter_name][property_index] = value
759
760        # Force the chart update when actual parameters changed
761        if model_column == 1:
762            self.recalculatePlotData()
763
764        # Update state stack
765        self.updateUndo()
766
767    def onHelp(self):
768        """
769        Show the "Fitting" section of help
770        """
771        tree_location = "/user/sasgui/perspectives/fitting/"
772
773        # Actual file will depend on the current tab
774        tab_id = self.tabFitting.currentIndex()
775        helpfile = "fitting.html"
776        if tab_id == 0:
777            helpfile = "fitting_help.html"
778        elif tab_id == 1:
779            helpfile = "residuals_help.html"
780        elif tab_id == 2:
781            helpfile = "resolution.html"
782        elif tab_id == 3:
783            helpfile = "pd/polydispersity.html"
784        elif tab_id == 4:
785            helpfile = "magnetism/magnetism.html"
786        help_location = tree_location + helpfile
787
788        self.showHelp(help_location)
789
790    def showHelp(self, url):
791        """
792        Calls parent's method for opening an HTML page
793        """
794        self.parent.showHelp(url)
795
796    def onDisplayMagneticAngles(self):
797        """
798        Display a simple image showing direction of magnetic angles
799        """
800        self.magneticAnglesWidget.show()
801
802    def onFit(self):
803        """
804        Perform fitting on the current data
805        """
806
807        # Data going in
808        data = self.logic.data
809        model = self.kernel_module
810        qmin = self.q_range_min
811        qmax = self.q_range_max
812        params_to_fit = self.parameters_to_fit
813
814        # Potential weights added directly to data
815        self.addWeightingToData(data)
816
817        # Potential smearing added
818        # Remember that smearing_min/max can be None ->
819        # deal with it until Python gets discriminated unions
820        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
821
822        # These should be updating somehow?
823        fit_id = 0
824        constraints = []
825        smearer = None
826        page_id = [210]
827        handler = None
828        batch_inputs = {}
829        batch_outputs = {}
830        list_page_id = [page_id]
831        #---------------------------------
832        if USING_TWISTED:
833            handler = None
834            updater = None
835        else:
836            handler = ConsoleUpdate(parent=self.parent,
837                                    manager=self,
838                                    improvement_delta=0.1)
839            updater = handler.update_fit
840
841        # Parameterize the fitter
842        fitters = []
843        for fit_index in self.all_data:
844            fitter = Fit()
845            data = GuiUtils.dataFromItem(fit_index)
846            try:
847                fitter.set_model(model, fit_id, params_to_fit, data=data,
848                             constraints=constraints)
849            except ValueError as ex:
850                logging.error("Setting model parameters failed with: %s" % ex)
851                return
852
853            qmin, qmax, _ = self.logic.computeRangeFromData(data)
854            fitter.set_data(data=data, id=fit_id, smearer=smearer, qmin=qmin,
855                            qmax=qmax)
856            fitter.select_problem_for_fit(id=fit_id, value=1)
857            fitter.fitter_id = page_id
858            fit_id += 1
859            fitters.append(fitter)
860
861        # Create the fitting thread, based on the fitter
862        completefn = self.batchFitComplete if self.is_batch_fitting else self.fitComplete
863
864        calc_fit = FitThread(handler=handler,
865                                fn=fitters,
866                                batch_inputs=batch_inputs,
867                                batch_outputs=batch_outputs,
868                                page_id=list_page_id,
869                                updatefn=updater,
870                                completefn=completefn)
871
872        if USING_TWISTED:
873            # start the trhrhread with twisted
874            calc_thread = threads.deferToThread(calc_fit.compute)
875            calc_thread.addCallback(self.fitComplete)
876            calc_thread.addErrback(self.fitFailed)
877        else:
878            # Use the old python threads + Queue
879            calc_fit.queue()
880            calc_fit.ready(2.5)
881
882
883        #disable the Fit button
884        self.cmdFit.setText('Running...')
885        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
886        self.cmdFit.setEnabled(False)
887
888    def updateFit(self):
889        """
890        """
891        print("UPDATE FIT")
892        pass
893
894    def fitFailed(self, reason):
895        """
896        """
897        print("FIT FAILED: ", reason)
898        pass
899
900    def batchFitComplete(self, result):
901        """
902        Receive and display batch fitting results
903        """
904        #re-enable the Fit button
905        self.cmdFit.setText("Fit")
906        self.cmdFit.setEnabled(True)
907
908        print ("BATCH FITTING FINISHED")
909        # Add the Qt version of wx.aui.AuiNotebook and populate it
910        pass
911
912    def fitComplete(self, result):
913        """
914        Receive and display fitting results
915        "result" is a tuple of actual result list and the fit time in seconds
916        """
917        #re-enable the Fit button
918        self.cmdFit.setText("Fit")
919        self.cmdFit.setEnabled(True)
920
921        assert result is not None
922
923        res_list = result[0][0]
924        res = res_list[0]
925        if res.fitness is None or \
926            not np.isfinite(res.fitness) or \
927            np.any(res.pvec is None) or \
928            not np.all(np.isfinite(res.pvec)):
929            msg = "Fitting did not converge!!!"
930            self.communicate.statusBarUpdateSignal.emit(msg)
931            logging.error(msg)
932            return
933
934        elapsed = result[1]
935        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
936        self.communicate.statusBarUpdateSignal.emit(msg)
937
938        self.chi2 = res.fitness
939        param_list = res.param_list # ['radius', 'radius.width']
940        param_values = res.pvec     # array([ 0.36221662,  0.0146783 ])
941        param_stderr = res.stderr   # array([ 1.71293015,  1.71294233])
942        params_and_errors = list(zip(param_values, param_stderr))
943        param_dict = dict(zip(param_list, params_and_errors))
944
945        # Dictionary of fitted parameter: value, error
946        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
947        self.updateModelFromList(param_dict)
948
949        self.updatePolyModelFromList(param_dict)
950
951        self.updateMagnetModelFromList(param_dict)
952
953        # update charts
954        self.onPlot()
955
956        # Read only value - we can get away by just printing it here
957        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
958        self.lblChi2Value.setText(chi2_repr)
959
960    def iterateOverModel(self, func):
961        """
962        Take func and throw it inside the model row loop
963        """
964        for row_i in range(self._model_model.rowCount()):
965            func(row_i)
966
967    def updateModelFromList(self, param_dict):
968        """
969        Update the model with new parameters, create the errors column
970        """
971        assert isinstance(param_dict, dict)
972        if not dict:
973            return
974
975        def updateFittedValues(row):
976            # Utility function for main model update
977            # internal so can use closure for param_dict
978            param_name = str(self._model_model.item(row, 0).text())
979            if param_name not in list(param_dict.keys()):
980                return
981            # modify the param value
982            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
983            self._model_model.item(row, 1).setText(param_repr)
984            if self.has_error_column:
985                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
986                self._model_model.item(row, 2).setText(error_repr)
987
988        def updatePolyValues(row):
989            # Utility function for updateof polydispersity part of the main model
990            param_name = str(self._model_model.item(row, 0).text())+'.width'
991            if param_name not in list(param_dict.keys()):
992                return
993            # modify the param value
994            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
995            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
996
997        def createErrorColumn(row):
998            # Utility function for error column update
999            item = QtGui.QStandardItem()
1000            def createItem(param_name):
1001                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1002                item.setText(error_repr)
1003            def curr_param():
1004                return str(self._model_model.item(row, 0).text())
1005
1006            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1007
1008            error_column.append(item)
1009
1010        # block signals temporarily, so we don't end up
1011        # updating charts with every single model change on the end of fitting
1012        self._model_model.blockSignals(True)
1013        self.iterateOverModel(updateFittedValues)
1014        self.iterateOverModel(updatePolyValues)
1015        self._model_model.blockSignals(False)
1016
1017        if self.has_error_column:
1018            return
1019
1020        error_column = []
1021        self.lstParams.itemDelegate().addErrorColumn()
1022        self.iterateOverModel(createErrorColumn)
1023
1024        # switch off reponse to model change
1025        self._model_model.blockSignals(True)
1026        self._model_model.insertColumn(2, error_column)
1027        self._model_model.blockSignals(False)
1028        FittingUtilities.addErrorHeadersToModel(self._model_model)
1029        # Adjust the table cells width.
1030        # TODO: find a way to dynamically adjust column width while resized expanding
1031        self.lstParams.resizeColumnToContents(0)
1032        self.lstParams.resizeColumnToContents(4)
1033        self.lstParams.resizeColumnToContents(5)
1034        self.lstParams.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
1035
1036        self.has_error_column = True
1037
1038    def updatePolyModelFromList(self, param_dict):
1039        """
1040        Update the polydispersity model with new parameters, create the errors column
1041        """
1042        assert isinstance(param_dict, dict)
1043        if not dict:
1044            return
1045
1046        def iterateOverPolyModel(func):
1047            """
1048            Take func and throw it inside the poly model row loop
1049            """
1050            for row_i in range(self._poly_model.rowCount()):
1051                func(row_i)
1052
1053        def updateFittedValues(row_i):
1054            # Utility function for main model update
1055            # internal so can use closure for param_dict
1056            if row_i >= self._poly_model.rowCount():
1057                return
1058            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1059            if param_name not in list(param_dict.keys()):
1060                return
1061            # modify the param value
1062            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1063            self._poly_model.item(row_i, 1).setText(param_repr)
1064            if self.has_poly_error_column:
1065                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1066                self._poly_model.item(row_i, 2).setText(error_repr)
1067
1068
1069        def createErrorColumn(row_i):
1070            # Utility function for error column update
1071            if row_i >= self._poly_model.rowCount():
1072                return
1073            item = QtGui.QStandardItem()
1074
1075            def createItem(param_name):
1076                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1077                item.setText(error_repr)
1078
1079            def poly_param():
1080                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1081
1082            [createItem(param_name) for param_name in list(param_dict.keys()) if poly_param() == param_name]
1083
1084            error_column.append(item)
1085
1086        # block signals temporarily, so we don't end up
1087        # updating charts with every single model change on the end of fitting
1088        self._poly_model.blockSignals(True)
1089        iterateOverPolyModel(updateFittedValues)
1090        self._poly_model.blockSignals(False)
1091
1092        if self.has_poly_error_column:
1093            return
1094
1095        self.lstPoly.itemDelegate().addErrorColumn()
1096        error_column = []
1097        iterateOverPolyModel(createErrorColumn)
1098
1099        # switch off reponse to model change
1100        self._poly_model.blockSignals(True)
1101        self._poly_model.insertColumn(2, error_column)
1102        self._poly_model.blockSignals(False)
1103        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
1104
1105        self.has_poly_error_column = True
1106
1107    def updateMagnetModelFromList(self, param_dict):
1108        """
1109        Update the magnetic model with new parameters, create the errors column
1110        """
1111        assert isinstance(param_dict, dict)
1112        if not dict:
1113            return
1114        if self._model_model.rowCount() == 0:
1115            return
1116
1117        def iterateOverMagnetModel(func):
1118            """
1119            Take func and throw it inside the magnet model row loop
1120            """
1121            for row_i in range(self._model_model.rowCount()):
1122                func(row_i)
1123
1124        def updateFittedValues(row):
1125            # Utility function for main model update
1126            # internal so can use closure for param_dict
1127            if self._magnet_model.item(row, 0) is None:
1128                return
1129            param_name = str(self._magnet_model.item(row, 0).text())
1130            if param_name not in list(param_dict.keys()):
1131                return
1132            # modify the param value
1133            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1134            self._magnet_model.item(row, 1).setText(param_repr)
1135            if self.has_magnet_error_column:
1136                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1137                self._magnet_model.item(row, 2).setText(error_repr)
1138
1139        def createErrorColumn(row):
1140            # Utility function for error column update
1141            item = QtGui.QStandardItem()
1142            def createItem(param_name):
1143                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1144                item.setText(error_repr)
1145            def curr_param():
1146                return str(self._magnet_model.item(row, 0).text())
1147
1148            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1149
1150            error_column.append(item)
1151
1152        # block signals temporarily, so we don't end up
1153        # updating charts with every single model change on the end of fitting
1154        self._magnet_model.blockSignals(True)
1155        iterateOverMagnetModel(updateFittedValues)
1156        self._magnet_model.blockSignals(False)
1157
1158        if self.has_magnet_error_column:
1159            return
1160
1161        self.lstMagnetic.itemDelegate().addErrorColumn()
1162        error_column = []
1163        iterateOverMagnetModel(createErrorColumn)
1164
1165        # switch off reponse to model change
1166        self._magnet_model.blockSignals(True)
1167        self._magnet_model.insertColumn(2, error_column)
1168        self._magnet_model.blockSignals(False)
1169        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1170
1171        self.has_magnet_error_column = True
1172
1173    def onPlot(self):
1174        """
1175        Plot the current set of data
1176        """
1177        # Regardless of previous state, this should now be `plot show` functionality only
1178        self.cmdPlot.setText("Show Plot")
1179        # Force data recalculation so existing charts are updated
1180        self.recalculatePlotData()
1181        self.showPlot()
1182
1183    def recalculatePlotData(self):
1184        """
1185        Generate a new dataset for model
1186        """
1187        if not self.data_is_loaded:
1188            self.createDefaultDataset()
1189        self.calculateQGridForModel()
1190
1191    def showPlot(self):
1192        """
1193        Show the current plot in MPL
1194        """
1195        # Show the chart if ready
1196        data_to_show = self.data if self.data_is_loaded else self.model_data
1197        if data_to_show is not None:
1198            self.communicate.plotRequestedSignal.emit([data_to_show])
1199
1200    def onOptionsUpdate(self):
1201        """
1202        Update local option values and replot
1203        """
1204        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1205            self.options_widget.state()
1206        # set Q range labels on the main tab
1207        self.lblMinRangeDef.setText(str(self.q_range_min))
1208        self.lblMaxRangeDef.setText(str(self.q_range_max))
1209        self.recalculatePlotData()
1210
1211    def setDefaultStructureCombo(self):
1212        """
1213        Fill in the structure factors combo box with defaults
1214        """
1215        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1216        factors = [factor[0] for factor in structure_factor_list]
1217        factors.insert(0, STRUCTURE_DEFAULT)
1218        self.cbStructureFactor.clear()
1219        self.cbStructureFactor.addItems(sorted(factors))
1220
1221    def createDefaultDataset(self):
1222        """
1223        Generate default Dataset 1D/2D for the given model
1224        """
1225        # Create default datasets if no data passed
1226        if self.is2D:
1227            qmax = self.q_range_max/np.sqrt(2)
1228            qstep = self.npts
1229            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
1230            return
1231        elif self.log_points:
1232            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
1233            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
1234            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
1235        else:
1236            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
1237                                   num=self.npts, endpoint=True)
1238        self.logic.createDefault1dData(interval, self.tab_id)
1239
1240    def readCategoryInfo(self):
1241        """
1242        Reads the categories in from file
1243        """
1244        self.master_category_dict = defaultdict(list)
1245        self.by_model_dict = defaultdict(list)
1246        self.model_enabled_dict = defaultdict(bool)
1247
1248        categorization_file = CategoryInstaller.get_user_file()
1249        if not os.path.isfile(categorization_file):
1250            categorization_file = CategoryInstaller.get_default_file()
1251        with open(categorization_file, 'rb') as cat_file:
1252            self.master_category_dict = json.load(cat_file)
1253            self.regenerateModelDict()
1254
1255        # Load the model dict
1256        models = load_standard_models()
1257        for model in models:
1258            self.models[model.name] = model
1259
1260    def regenerateModelDict(self):
1261        """
1262        Regenerates self.by_model_dict which has each model name as the
1263        key and the list of categories belonging to that model
1264        along with the enabled mapping
1265        """
1266        self.by_model_dict = defaultdict(list)
1267        for category in self.master_category_dict:
1268            for (model, enabled) in self.master_category_dict[category]:
1269                self.by_model_dict[model].append(category)
1270                self.model_enabled_dict[model] = enabled
1271
1272    def addBackgroundToModel(self, model):
1273        """
1274        Adds background parameter with default values to the model
1275        """
1276        assert isinstance(model, QtGui.QStandardItemModel)
1277        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
1278        FittingUtilities.addCheckedListToModel(model, checked_list)
1279        last_row = model.rowCount()-1
1280        model.item(last_row, 0).setEditable(False)
1281        model.item(last_row, 4).setEditable(False)
1282
1283    def addScaleToModel(self, model):
1284        """
1285        Adds scale parameter with default values to the model
1286        """
1287        assert isinstance(model, QtGui.QStandardItemModel)
1288        checked_list = ['scale', '1.0', '0.0', 'inf', '']
1289        FittingUtilities.addCheckedListToModel(model, checked_list)
1290        last_row = model.rowCount()-1
1291        model.item(last_row, 0).setEditable(False)
1292        model.item(last_row, 4).setEditable(False)
1293
1294    def addWeightingToData(self, data):
1295        """
1296        Adds weighting contribution to fitting data
1297        """
1298        # Send original data for weighting
1299        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
1300        update_module = data.err_data if self.is2D else data.dy
1301        # Overwrite relevant values in data
1302        update_module = weight
1303
1304    def updateQRange(self):
1305        """
1306        Updates Q Range display
1307        """
1308        if self.data_is_loaded:
1309            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
1310        # set Q range labels on the main tab
1311        self.lblMinRangeDef.setText(str(self.q_range_min))
1312        self.lblMaxRangeDef.setText(str(self.q_range_max))
1313        # set Q range labels on the options tab
1314        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
1315
1316    def SASModelToQModel(self, model_name, structure_factor=None):
1317        """
1318        Setting model parameters into table based on selected category
1319        """
1320        # Crete/overwrite model items
1321        self._model_model.clear()
1322
1323        # First, add parameters from the main model
1324        if model_name is not None:
1325            self.fromModelToQModel(model_name)
1326
1327        # Then, add structure factor derived parameters
1328        if structure_factor is not None and structure_factor != "None":
1329            if model_name is None:
1330                # Instantiate the current sasmodel for SF-only models
1331                self.kernel_module = self.models[structure_factor]()
1332            self.fromStructureFactorToQModel(structure_factor)
1333        else:
1334            # Allow the SF combobox visibility for the given sasmodel
1335            self.enableStructureFactorControl(structure_factor)
1336
1337        # Then, add multishells
1338        if model_name is not None:
1339            # Multishell models need additional treatment
1340            self.addExtraShells()
1341
1342        # Add polydispersity to the model
1343        self.setPolyModel()
1344        # Add magnetic parameters to the model
1345        self.setMagneticModel()
1346
1347        # Adjust the table cells width
1348        self.lstParams.resizeColumnToContents(0)
1349        self.lstParams.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
1350
1351        # Now we claim the model has been loaded
1352        self.model_is_loaded = True
1353
1354        # (Re)-create headers
1355        FittingUtilities.addHeadersToModel(self._model_model)
1356        self.lstParams.header().setFont(self.boldFont)
1357
1358        # Update Q Ranges
1359        self.updateQRange()
1360
1361    def fromModelToQModel(self, model_name):
1362        """
1363        Setting model parameters into QStandardItemModel based on selected _model_
1364        """
1365        kernel_module = generate.load_kernel_module(model_name)
1366        self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
1367
1368        # Instantiate the current sasmodel
1369        self.kernel_module = self.models[model_name]()
1370
1371        # Explicitly add scale and background with default values
1372        temp_undo_state = self.undo_supported
1373        self.undo_supported = False
1374        self.addScaleToModel(self._model_model)
1375        self.addBackgroundToModel(self._model_model)
1376        self.undo_supported = temp_undo_state
1377
1378        self.shell_names = self.shellNamesList()
1379
1380        # Update the QModel
1381        new_rows = FittingUtilities.addParametersToModel(self.model_parameters, self.kernel_module, self.is2D)
1382
1383        for row in new_rows:
1384            self._model_model.appendRow(row)
1385        # Update the counter used for multishell display
1386        self._last_model_row = self._model_model.rowCount()
1387
1388    def fromStructureFactorToQModel(self, structure_factor):
1389        """
1390        Setting model parameters into QStandardItemModel based on selected _structure factor_
1391        """
1392        structure_module = generate.load_kernel_module(structure_factor)
1393        structure_parameters = modelinfo.make_parameter_table(getattr(structure_module, 'parameters', []))
1394        structure_kernel = self.models[structure_factor]()
1395
1396        self.kernel_module._model_info = product.make_product_info(self.kernel_module._model_info, structure_kernel._model_info)
1397
1398        new_rows = FittingUtilities.addSimpleParametersToModel(structure_parameters, self.is2D)
1399        for row in new_rows:
1400            self._model_model.appendRow(row)
1401        # Update the counter used for multishell display
1402        self._last_model_row = self._model_model.rowCount()
1403
1404    def onMainParamsChange(self, item):
1405        """
1406        Callback method for updating the sasmodel parameters with the GUI values
1407        """
1408        model_column = item.column()
1409
1410        if model_column == 0:
1411            self.checkboxSelected(item)
1412            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
1413            # Update state stack
1414            self.updateUndo()
1415            return
1416
1417        model_row = item.row()
1418        name_index = self._model_model.index(model_row, 0)
1419
1420        # Extract changed value.
1421        try:
1422            value = GuiUtils.toDouble(item.text())
1423        except TypeError:
1424            # Unparsable field
1425            return
1426
1427        parameter_name = str(self._model_model.data(name_index)) # sld, background etc.
1428
1429        # Update the parameter value - note: this supports +/-inf as well
1430        self.kernel_module.params[parameter_name] = value
1431
1432        # Update the parameter value - note: this supports +/-inf as well
1433        param_column = self.lstParams.itemDelegate().param_value
1434        min_column = self.lstParams.itemDelegate().param_min
1435        max_column = self.lstParams.itemDelegate().param_max
1436        if model_column == param_column:
1437            self.kernel_module.setParam(parameter_name, value)
1438        elif model_column == min_column:
1439            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1440            self.kernel_module.details[parameter_name][1] = value
1441        elif model_column == max_column:
1442            self.kernel_module.details[parameter_name][2] = value
1443        else:
1444            # don't update the chart
1445            return
1446
1447        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
1448        # TODO: multishell params in self.kernel_module.details[??] = value
1449
1450        # Force the chart update when actual parameters changed
1451        if model_column == 1:
1452            self.recalculatePlotData()
1453
1454        # Update state stack
1455        self.updateUndo()
1456
1457    def isCheckable(self, row):
1458        return self._model_model.item(row, 0).isCheckable()
1459
1460    def checkboxSelected(self, item):
1461        # Assure we're dealing with checkboxes
1462        if not item.isCheckable():
1463            return
1464        status = item.checkState()
1465
1466        # If multiple rows selected - toggle all of them, filtering uncheckable
1467        # Switch off signaling from the model to avoid recursion
1468        self._model_model.blockSignals(True)
1469        # Convert to proper indices and set requested enablement
1470        self.setParameterSelection(status)
1471        #[self._model_model.item(row, 0).setCheckState(status) for row in self.selectedParameters()]
1472        self._model_model.blockSignals(False)
1473
1474        # update the list of parameters to fit
1475        main_params = self.checkedListFromModel(self._model_model)
1476        poly_params = self.checkedListFromModel(self._poly_model)
1477        magnet_params = self.checkedListFromModel(self._magnet_model)
1478
1479        # Retrieve poly params names
1480        poly_params = [param.rsplit()[-1] + '.width' for param in poly_params]
1481
1482        self.parameters_to_fit = main_params + poly_params + magnet_params
1483
1484    def checkedListFromModel(self, model):
1485        """
1486        Returns list of checked parameters for given model
1487        """
1488        def isChecked(row):
1489            return model.item(row, 0).checkState() == QtCore.Qt.Checked
1490
1491        return [str(model.item(row_index, 0).text())
1492                for row_index in range(model.rowCount())
1493                if isChecked(row_index)]
1494
1495    def nameForFittedData(self, name):
1496        """
1497        Generate name for the current fit
1498        """
1499        if self.is2D:
1500            name += "2d"
1501        name = "M%i [%s]" % (self.tab_id, name)
1502        return name
1503
1504    def createNewIndex(self, fitted_data):
1505        """
1506        Create a model or theory index with passed Data1D/Data2D
1507        """
1508        if self.data_is_loaded:
1509            if not fitted_data.name:
1510                name = self.nameForFittedData(self.data.filename)
1511                fitted_data.title = name
1512                fitted_data.name = name
1513                fitted_data.filename = name
1514                fitted_data.symbol = "Line"
1515            self.updateModelIndex(fitted_data)
1516        else:
1517            name = self.nameForFittedData(self.kernel_module.name)
1518            fitted_data.title = name
1519            fitted_data.name = name
1520            fitted_data.filename = name
1521            fitted_data.symbol = "Line"
1522            self.createTheoryIndex(fitted_data)
1523
1524    def updateModelIndex(self, fitted_data):
1525        """
1526        Update a QStandardModelIndex containing model data
1527        """
1528        name = self.nameFromData(fitted_data)
1529        # Make this a line if no other defined
1530        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
1531            fitted_data.symbol = 'Line'
1532        # Notify the GUI manager so it can update the main model in DataExplorer
1533        GuiUtils.updateModelItemWithPlot(self._index, fitted_data, name)
1534
1535    def createTheoryIndex(self, fitted_data):
1536        """
1537        Create a QStandardModelIndex containing model data
1538        """
1539        name = self.nameFromData(fitted_data)
1540        # Notify the GUI manager so it can create the theory model in DataExplorer
1541        new_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
1542        self.communicate.updateTheoryFromPerspectiveSignal.emit(new_item)
1543
1544    def nameFromData(self, fitted_data):
1545        """
1546        Return name for the dataset. Terribly impure function.
1547        """
1548        if fitted_data.name is None:
1549            name = self.nameForFittedData(self.logic.data.filename)
1550            fitted_data.title = name
1551            fitted_data.name = name
1552            fitted_data.filename = name
1553        else:
1554            name = fitted_data.name
1555        return name
1556
1557    def methodCalculateForData(self):
1558        '''return the method for data calculation'''
1559        return Calc1D if isinstance(self.data, Data1D) else Calc2D
1560
1561    def methodCompleteForData(self):
1562        '''return the method for result parsin on calc complete '''
1563        return self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1564
1565    def calculateQGridForModel(self):
1566        """
1567        Prepare the fitting data object, based on current ModelModel
1568        """
1569        if self.kernel_module is None:
1570            return
1571        # Awful API to a backend method.
1572        method = self.methodCalculateForData()(data=self.data,
1573                                               model=self.kernel_module,
1574                                               page_id=0,
1575                                               qmin=self.q_range_min,
1576                                               qmax=self.q_range_max,
1577                                               smearer=None,
1578                                               state=None,
1579                                               weight=None,
1580                                               fid=None,
1581                                               toggle_mode_on=False,
1582                                               completefn=None,
1583                                               update_chisqr=True,
1584                                               exception_handler=self.calcException,
1585                                               source=None)
1586
1587        calc_thread = threads.deferToThread(method.compute)
1588        calc_thread.addCallback(self.methodCompleteForData())
1589        calc_thread.addErrback(self.calculateDataFailed)
1590
1591    def calculateDataFailed(self, reason):
1592        """
1593        Thread returned error
1594        """
1595        print("Calculate Data failed with ", reason)
1596
1597    def complete1D(self, return_data):
1598        """
1599        Plot the current 1D data
1600        """
1601        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
1602        self.calculateResiduals(fitted_data)
1603        self.model_data = fitted_data
1604
1605    def complete2D(self, return_data):
1606        """
1607        Plot the current 2D data
1608        """
1609        fitted_data = self.logic.new2DPlot(return_data)
1610        self.calculateResiduals(fitted_data)
1611        self.model_data = fitted_data
1612
1613    def calculateResiduals(self, fitted_data):
1614        """
1615        Calculate and print Chi2 and display chart of residuals
1616        """
1617        # Create a new index for holding data
1618        fitted_data.symbol = "Line"
1619
1620        # Modify fitted_data with weighting
1621        self.addWeightingToData(fitted_data)
1622
1623        self.createNewIndex(fitted_data)
1624        # Calculate difference between return_data and logic.data
1625        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
1626        # Update the control
1627        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
1628        self.lblChi2Value.setText(chi2_repr)
1629
1630        self.communicate.plotUpdateSignal.emit([fitted_data])
1631
1632        # Plot residuals if actual data
1633        if not self.data_is_loaded:
1634            return
1635
1636        residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
1637        residuals_plot.id = "Residual " + residuals_plot.id
1638        self.createNewIndex(residuals_plot)
1639        #self.communicate.plotUpdateSignal.emit([residuals_plot])
1640
1641    def calcException(self, etype, value, tb):
1642        """
1643        Thread threw an exception.
1644        """
1645        # TODO: remimplement thread cancellation
1646        logging.error("".join(traceback.format_exception(etype, value, tb)))
1647
1648    def setTableProperties(self, table):
1649        """
1650        Setting table properties
1651        """
1652        # Table properties
1653        table.verticalHeader().setVisible(False)
1654        table.setAlternatingRowColors(True)
1655        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
1656        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
1657        table.resizeColumnsToContents()
1658
1659        # Header
1660        header = table.horizontalHeader()
1661        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
1662        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
1663
1664        # Qt5: the following 2 lines crash - figure out why!
1665        # Resize column 0 and 7 to content
1666        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
1667        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
1668
1669    def setPolyModel(self):
1670        """
1671        Set polydispersity values
1672        """
1673        if not self.model_parameters:
1674            return
1675        self._poly_model.clear()
1676
1677        [self.setPolyModelParameters(i, param) for i, param in \
1678            enumerate(self.model_parameters.form_volume_parameters) if param.polydisperse]
1679        FittingUtilities.addPolyHeadersToModel(self._poly_model)
1680
1681    def setPolyModelParameters(self, i, param):
1682        """
1683        Standard of multishell poly parameter driver
1684        """
1685        param_name = param.name
1686        # see it the parameter is multishell
1687        if '[' in param.name:
1688            # Skip empty shells
1689            if self.current_shell_displayed == 0:
1690                return
1691            else:
1692                # Create as many entries as current shells
1693                for ishell in range(1, self.current_shell_displayed+1):
1694                    # Remove [n] and add the shell numeral
1695                    name = param_name[0:param_name.index('[')] + str(ishell)
1696                    self.addNameToPolyModel(i, name)
1697        else:
1698            # Just create a simple param entry
1699            self.addNameToPolyModel(i, param_name)
1700
1701    def addNameToPolyModel(self, i, param_name):
1702        """
1703        Creates a checked row in the poly model with param_name
1704        """
1705        # Polydisp. values from the sasmodel
1706        width = self.kernel_module.getParam(param_name + '.width')
1707        npts = self.kernel_module.getParam(param_name + '.npts')
1708        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
1709        _, min, max = self.kernel_module.details[param_name]
1710
1711        # Construct a row with polydisp. related variable.
1712        # This will get added to the polydisp. model
1713        # Note: last argument needs extra space padding for decent display of the control
1714        checked_list = ["Distribution of " + param_name, str(width),
1715                        str(min), str(max),
1716                        str(npts), str(nsigs), "gaussian      ",'']
1717        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
1718
1719        # All possible polydisp. functions as strings in combobox
1720        func = QtWidgets.QComboBox()
1721        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
1722        # Set the default index
1723        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
1724        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
1725        self.lstPoly.setIndexWidget(ind, func)
1726        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
1727
1728    def onPolyFilenameChange(self, row_index):
1729        """
1730        Respond to filename_updated signal from the delegate
1731        """
1732        # For the given row, invoke the "array" combo handler
1733        array_caption = 'array'
1734
1735        # Get the combo box reference
1736        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
1737        widget = self.lstPoly.indexWidget(ind)
1738
1739        # Update the combo box so it displays "array"
1740        widget.blockSignals(True)
1741        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
1742        widget.blockSignals(False)
1743
1744        # Invoke the file reader
1745        self.onPolyComboIndexChange(array_caption, row_index)
1746
1747    def onPolyComboIndexChange(self, combo_string, row_index):
1748        """
1749        Modify polydisp. defaults on function choice
1750        """
1751        # Get npts/nsigs for current selection
1752        param = self.model_parameters.form_volume_parameters[row_index]
1753        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
1754        combo_box = self.lstPoly.indexWidget(file_index)
1755
1756        def updateFunctionCaption(row):
1757            # Utility function for update of polydispersity function name in the main model
1758            param_name = str(self._model_model.item(row, 0).text())
1759            if param_name !=  param.name:
1760                return
1761            # Modify the param value
1762            self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
1763
1764        if combo_string == 'array':
1765            try:
1766                self.loadPolydispArray(row_index)
1767                # Update main model for display
1768                self.iterateOverModel(updateFunctionCaption)
1769                # disable the row
1770                lo = self.lstPoly.itemDelegate().poly_pd
1771                hi = self.lstPoly.itemDelegate().poly_function
1772                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
1773                return
1774            except IOError:
1775                combo_box.setCurrentIndex(self.orig_poly_index)
1776                # Pass for cancel/bad read
1777                pass
1778
1779        # Enable the row in case it was disabled by Array
1780        self._poly_model.blockSignals(True)
1781        max_range = self.lstPoly.itemDelegate().poly_filename
1782        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
1783        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
1784        self._poly_model.setData(file_index, "")
1785        self._poly_model.blockSignals(False)
1786
1787        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
1788        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
1789
1790        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
1791        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
1792
1793        self._poly_model.setData(npts_index, npts)
1794        self._poly_model.setData(nsigs_index, nsigs)
1795
1796        self.iterateOverModel(updateFunctionCaption)
1797        self.orig_poly_index = combo_box.currentIndex()
1798
1799    def loadPolydispArray(self, row_index):
1800        """
1801        Show the load file dialog and loads requested data into state
1802        """
1803        datafile = QtWidgets.QFileDialog.getOpenFileName(
1804            self, "Choose a weight file", "", "All files (*.*)", None,
1805            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
1806
1807        if not datafile:
1808            logging.info("No weight data chosen.")
1809            raise IOError
1810
1811        values = []
1812        weights = []
1813        def appendData(data_tuple):
1814            """
1815            Fish out floats from a tuple of strings
1816            """
1817            try:
1818                values.append(float(data_tuple[0]))
1819                weights.append(float(data_tuple[1]))
1820            except (ValueError, IndexError):
1821                # just pass through if line with bad data
1822                return
1823
1824        with open(datafile, 'r') as column_file:
1825            column_data = [line.rstrip().split() for line in column_file.readlines()]
1826            [appendData(line) for line in column_data]
1827
1828        # If everything went well - update the sasmodel values
1829        self.disp_model = POLYDISPERSITY_MODELS['array']()
1830        self.disp_model.set_weights(np.array(values), np.array(weights))
1831        # + update the cell with filename
1832        fname = os.path.basename(str(datafile))
1833        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
1834        self._poly_model.setData(fname_index, fname)
1835
1836    def setMagneticModel(self):
1837        """
1838        Set magnetism values on model
1839        """
1840        if not self.model_parameters:
1841            return
1842        self._magnet_model.clear()
1843        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
1844            self.model_parameters.call_parameters if param.type == 'magnetic']
1845        FittingUtilities.addHeadersToModel(self._magnet_model)
1846
1847    def shellNamesList(self):
1848        """
1849        Returns list of names of all multi-shell parameters
1850        E.g. for sld[n], radius[n], n=1..3 it will return
1851        [sld1, sld2, sld3, radius1, radius2, radius3]
1852        """
1853        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
1854        top_index = self.kernel_module.multiplicity_info.number
1855        shell_names = []
1856        for i in range(1, top_index+1):
1857            for name in multi_names:
1858                shell_names.append(name+str(i))
1859        return shell_names
1860
1861    def addCheckedMagneticListToModel(self, param, model):
1862        """
1863        Wrapper for model update with a subset of magnetic parameters
1864        """
1865        if param.name[param.name.index(':')+1:] in self.shell_names:
1866            # check if two-digit shell number
1867            try:
1868                shell_index = int(param.name[-2:])
1869            except ValueError:
1870                shell_index = int(param.name[-1:])
1871
1872            if shell_index > self.current_shell_displayed:
1873                return
1874
1875        checked_list = [param.name,
1876                        str(param.default),
1877                        str(param.limits[0]),
1878                        str(param.limits[1]),
1879                        param.units]
1880
1881        FittingUtilities.addCheckedListToModel(model, checked_list)
1882
1883    def enableStructureFactorControl(self, structure_factor):
1884        """
1885        Add structure factors to the list of parameters
1886        """
1887        if self.kernel_module.is_form_factor or structure_factor == 'None':
1888            self.enableStructureCombo()
1889        else:
1890            self.disableStructureCombo()
1891
1892    def addExtraShells(self):
1893        """
1894        Add a combobox for multiple shell display
1895        """
1896        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
1897
1898        if param_length == 0:
1899            return
1900
1901        # cell 1: variable name
1902        item1 = QtGui.QStandardItem(param_name)
1903
1904        func = QtWidgets.QComboBox()
1905        # Available range of shells displayed in the combobox
1906        func.addItems([str(i) for i in range(param_length+1)])
1907
1908        # Respond to index change
1909        func.currentIndexChanged.connect(self.modifyShellsInList)
1910
1911        # cell 2: combobox
1912        item2 = QtGui.QStandardItem()
1913        self._model_model.appendRow([item1, item2])
1914
1915        # Beautify the row:  span columns 2-4
1916        shell_row = self._model_model.rowCount()
1917        shell_index = self._model_model.index(shell_row-1, 1)
1918
1919        self.lstParams.setIndexWidget(shell_index, func)
1920        self._last_model_row = self._model_model.rowCount()
1921
1922        # Set the index to the state-kept value
1923        func.setCurrentIndex(self.current_shell_displayed
1924                             if self.current_shell_displayed < func.count() else 0)
1925
1926    def modifyShellsInList(self, index):
1927        """
1928        Add/remove additional multishell parameters
1929        """
1930        # Find row location of the combobox
1931        last_row = self._last_model_row
1932        remove_rows = self._model_model.rowCount() - last_row
1933
1934        if remove_rows > 1:
1935            self._model_model.removeRows(last_row, remove_rows)
1936
1937        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
1938        self.current_shell_displayed = index
1939
1940        # Update relevant models
1941        self.setPolyModel()
1942        self.setMagneticModel()
1943
1944    def readFitPage(self, fp):
1945        """
1946        Read in state from a fitpage object and update GUI
1947        """
1948        assert isinstance(fp, FitPage)
1949        # Main tab info
1950        self.logic.data.filename = fp.filename
1951        self.data_is_loaded = fp.data_is_loaded
1952        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
1953        self.chkMagnetism.setCheckState(fp.is_magnetic)
1954        self.chk2DView.setCheckState(fp.is2D)
1955
1956        # Update the comboboxes
1957        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
1958        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
1959        if fp.current_factor:
1960            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
1961
1962        self.chi2 = fp.chi2
1963
1964        # Options tab
1965        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
1966        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
1967        self.npts = fp.fit_options[fp.NPTS]
1968        self.log_points = fp.fit_options[fp.LOG_POINTS]
1969        self.weighting = fp.fit_options[fp.WEIGHTING]
1970
1971        # Models
1972        self._model_model = fp.model_model
1973        self._poly_model = fp.poly_model
1974        self._magnet_model = fp.magnetism_model
1975
1976        # Resolution tab
1977        smearing = fp.smearing_options[fp.SMEARING_OPTION]
1978        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
1979        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
1980        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
1981        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
1982
1983        # TODO: add polidyspersity and magnetism
1984
1985    def saveToFitPage(self, fp):
1986        """
1987        Write current state to the given fitpage
1988        """
1989        assert isinstance(fp, FitPage)
1990
1991        # Main tab info
1992        fp.filename = self.logic.data.filename
1993        fp.data_is_loaded = self.data_is_loaded
1994        fp.is_polydisperse = self.chkPolydispersity.isChecked()
1995        fp.is_magnetic = self.chkMagnetism.isChecked()
1996        fp.is2D = self.chk2DView.isChecked()
1997        fp.data = self.data
1998
1999        # Use current models - they contain all the required parameters
2000        fp.model_model = self._model_model
2001        fp.poly_model = self._poly_model
2002        fp.magnetism_model = self._magnet_model
2003
2004        if self.cbCategory.currentIndex() != 0:
2005            fp.current_category = str(self.cbCategory.currentText())
2006            fp.current_model = str(self.cbModel.currentText())
2007
2008        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
2009            fp.current_factor = str(self.cbStructureFactor.currentText())
2010        else:
2011            fp.current_factor = ''
2012
2013        fp.chi2 = self.chi2
2014        fp.parameters_to_fit = self.parameters_to_fit
2015        fp.kernel_module = self.kernel_module
2016
2017        # Algorithm options
2018        # fp.algorithm = self.parent.fit_options.selected_id
2019
2020        # Options tab
2021        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
2022        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
2023        fp.fit_options[fp.NPTS] = self.npts
2024        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
2025        fp.fit_options[fp.LOG_POINTS] = self.log_points
2026        fp.fit_options[fp.WEIGHTING] = self.weighting
2027
2028        # Resolution tab
2029        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
2030        fp.smearing_options[fp.SMEARING_OPTION] = smearing
2031        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
2032        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
2033        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
2034
2035        # TODO: add polidyspersity and magnetism
2036
2037
2038    def updateUndo(self):
2039        """
2040        Create a new state page and add it to the stack
2041        """
2042        if self.undo_supported:
2043            self.pushFitPage(self.currentState())
2044
2045    def currentState(self):
2046        """
2047        Return fit page with current state
2048        """
2049        new_page = FitPage()
2050        self.saveToFitPage(new_page)
2051
2052        return new_page
2053
2054    def pushFitPage(self, new_page):
2055        """
2056        Add a new fit page object with current state
2057        """
2058        self.page_stack.append(new_page)
2059
2060    def popFitPage(self):
2061        """
2062        Remove top fit page from stack
2063        """
2064        if self.page_stack:
2065            self.page_stack.pop()
2066
Note: See TracBrowser for help on using the repository browser.