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

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 378e808 was 378e808, 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: 77.1 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
10
11from PyQt5 import QtCore
12from PyQt5 import QtGui
13from PyQt5 import QtWidgets
14from PyQt5 import QtWebKitWidgets
15# Officially QtWebEngineWidgets are the way to display HTML in Qt5,
16# but this module isn't ported to PyQt5 yet...
17# let's wait. In the meantime no Help.
18#from PyQt5 import QtWebEngineWidgets
19
20from sasmodels import product
21from sasmodels import generate
22from sasmodels import modelinfo
23from sasmodels.sasview_model import load_standard_models
24from sasmodels.weights import MODELS as POLYDISPERSITY_MODELS
25
26from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
27
28import sas.qtgui.Utilities.GuiUtils as GuiUtils
29from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
30from sas.qtgui.Plotting.PlotterData import Data1D
31from sas.qtgui.Plotting.PlotterData import Data2D
32
33from sas.qtgui.Perspectives.Fitting.UI.FittingWidgetUI import Ui_FittingWidgetUI
34from sas.qtgui.Perspectives.Fitting.FitThread import FitThread
35from sas.qtgui.Perspectives.Fitting.ConsoleUpdate import ConsoleUpdate
36
37from sas.qtgui.Perspectives.Fitting.ModelThread import Calc1D
38from sas.qtgui.Perspectives.Fitting.ModelThread import Calc2D
39from sas.qtgui.Perspectives.Fitting.FittingLogic import FittingLogic
40from sas.qtgui.Perspectives.Fitting import FittingUtilities
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
47
48
49TAB_MAGNETISM = 4
50TAB_POLY = 3
51CATEGORY_DEFAULT = "Choose category..."
52CATEGORY_STRUCTURE = "Structure Factor"
53STRUCTURE_DEFAULT = "None"
54
55DEFAULT_POLYDISP_FUNCTION = 'gaussian'
56
57USING_TWISTED = True
58#USING_TWISTED = False
59
60class ToolTippedItemModel(QtGui.QStandardItemModel):
61    """
62    Subclass from QStandardItemModel to allow displaying tooltips in
63    QTableView model.
64    """
65    def __init__(self, parent=None):
66        QtGui.QStandardItemModel.__init__(self,parent)
67
68    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
69        """
70        Displays tooltip for each column's header
71        :param section:
72        :param orientation:
73        :param role:
74        :return:
75        """
76        if role == QtCore.Qt.ToolTipRole:
77            if orientation == QtCore.Qt.Horizontal:
78                return str(self.header_tooltips[section])
79
80        return QtGui.QStandardItemModel.headerData(self, section, orientation, role)
81
82class FittingWidget(QtWidgets.QWidget, Ui_FittingWidgetUI):
83    """
84    Main widget for selecting form and structure factor models
85    """
86    def __init__(self, parent=None, data=None, tab_id=1):
87
88        super(FittingWidget, self).__init__()
89
90        # Necessary globals
91        self.parent = parent
92
93        # Which tab is this widget displayed in?
94        self.tab_id = tab_id
95
96        # Main Data[12]D holder
97        self.logic = FittingLogic()
98
99        # Globals
100        self.initializeGlobals()
101
102        # Main GUI setup up
103        self.setupUi(self)
104        self.setWindowTitle("Fitting")
105
106        # Set up tabs widgets
107        self.initializeWidgets()
108
109        # Set up models and views
110        self.initializeModels()
111
112        # Defaults for the structure factors
113        self.setDefaultStructureCombo()
114
115        # Make structure factor and model CBs disabled
116        self.disableModelCombo()
117        self.disableStructureCombo()
118
119        # Generate the category list for display
120        self.initializeCategoryCombo()
121
122        # Connect signals to controls
123        self.initializeSignals()
124
125        # Initial control state
126        self.initializeControls()
127
128        # Display HTML content
129        self.helpView = QtWebKitWidgets.QWebView()
130
131        # New font to display angstrom symbol
132        new_font = 'font-family: -apple-system, "Helvetica Neue", "Ubuntu";'
133        self.label_17.setStyleSheet(new_font)
134        self.label_19.setStyleSheet(new_font)
135
136        self._index = None
137        if data is not None:
138            self.data = data
139
140    @property
141    def data(self):
142        return self.logic.data
143
144    @data.setter
145    def data(self, value):
146        """ data setter """
147        # Value is either a list of indices for batch fitting or a simple index
148        # for standard fitting. Assure we have a list, regardless.
149        if isinstance(value, list):
150            self.is_batch_fitting = True
151        else:
152            value = [value]
153
154        assert isinstance(value[0], QtGui.QStandardItem)
155        # _index contains the QIndex with data
156        self._index = value[0]
157
158        # Keep reference to all datasets for batch
159        self.all_data = value
160
161        # Update logics with data items
162        # Logics.data contains only a single Data1D/Data2D object
163        self.logic.data = GuiUtils.dataFromItem(value[0])
164
165        # Overwrite data type descriptor
166        self.is2D = True if isinstance(self.logic.data, Data2D) else False
167
168        # Let others know we're full of data now
169        self.data_is_loaded = True
170
171        # Enable/disable UI components
172        self.setEnablementOnDataLoad()
173
174    def initializeGlobals(self):
175        """
176        Initialize global variables used in this class
177        """
178        # SasModel is loaded
179        self.model_is_loaded = False
180        # Data[12]D passed and set
181        self.data_is_loaded = False
182        # Batch/single fitting
183        self.is_batch_fitting = False
184        self.is_chain_fitting = False
185        # Current SasModel in view
186        self.kernel_module = None
187        # Current SasModel view dimension
188        self.is2D = False
189        # Current SasModel is multishell
190        self.model_has_shells = False
191        # Utility variable to enable unselectable option in category combobox
192        self._previous_category_index = 0
193        # Utility variable for multishell display
194        self._last_model_row = 0
195        # Dictionary of {model name: model class} for the current category
196        self.models = {}
197        # Parameters to fit
198        self.parameters_to_fit = None
199        # Fit options
200        self.q_range_min = 0.005
201        self.q_range_max = 0.1
202        self.npts = 25
203        self.log_points = False
204        self.weighting = 0
205        self.chi2 = None
206        # Does the control support UNDO/REDO
207        # temporarily off
208        self.undo_supported = False
209        self.page_stack = []
210        self.all_data = []
211        # Polydisp widget table default index for function combobox
212        self.orig_poly_index = 3
213
214        # Data for chosen model
215        self.model_data = None
216
217        # Which shell is being currently displayed?
218        self.current_shell_displayed = 0
219        # List of all shell-unique parameters
220        self.shell_names = []
221
222        # Error column presence in parameter display
223        self.has_error_column = False
224        self.has_poly_error_column = False
225        self.has_magnet_error_column = False
226
227        # signal communicator
228        self.communicate = self.parent.communicate
229
230    def initializeWidgets(self):
231        """
232        Initialize widgets for tabs
233        """
234        # Options widget
235        layout = QtWidgets.QGridLayout()
236        self.options_widget = OptionsWidget(self, self.logic)
237        layout.addWidget(self.options_widget)
238        self.tabOptions.setLayout(layout)
239
240        # Smearing widget
241        layout = QtWidgets.QGridLayout()
242        self.smearing_widget = SmearingWidget(self)
243        layout.addWidget(self.smearing_widget)
244        self.tabResolution.setLayout(layout)
245
246        # Define bold font for use in various controls
247        self.boldFont = QtGui.QFont()
248        self.boldFont.setBold(True)
249
250        # Set data label
251        self.label.setFont(self.boldFont)
252        self.label.setText("No data loaded")
253        self.lblFilename.setText("")
254
255        # Magnetic angles explained in one picture
256        self.magneticAnglesWidget = QtWidgets.QWidget()
257        labl = QtWidgets.QLabel(self.magneticAnglesWidget)
258        pixmap = QtGui.QPixmap(GuiUtils.IMAGES_DIRECTORY_LOCATION + '/M_angles_pic.bmp')
259        labl.setPixmap(pixmap)
260        self.magneticAnglesWidget.setFixedSize(pixmap.width(), pixmap.height())
261
262    def initializeModels(self):
263        """
264        Set up models and views
265        """
266        # Set the main models
267        # We can't use a single model here, due to restrictions on flattening
268        # the model tree with subclassed QAbstractProxyModel...
269        self._model_model = ToolTippedItemModel()
270        self._poly_model = ToolTippedItemModel()
271        self._magnet_model = ToolTippedItemModel()
272
273        # Param model displayed in param list
274        self.lstParams.setModel(self._model_model)
275        self.readCategoryInfo()
276
277        self.model_parameters = None
278
279        # Delegates for custom editing and display
280        self.lstParams.setItemDelegate(ModelViewDelegate(self))
281
282        self.lstParams.setAlternatingRowColors(True)
283        stylesheet = """
284
285            QTreeView {
286                paint-alternating-row-colors-for-empty-area:0;
287            }
288
289            QTreeView::item:hover {
290                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1);
291                border: 1px solid #bfcde4;
292            }
293
294            QTreeView::item:selected {
295                border: 1px solid #567dbc;
296            }
297
298            QTreeView::item:selected:active{
299                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6ea1f1, stop: 1 #567dbc);
300            }
301
302            QTreeView::item:selected:!active {
303                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6b9be8, stop: 1 #577fbf);
304            }
305           """
306        self.lstParams.setStyleSheet(stylesheet)
307        self.lstParams.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
308        self.lstParams.customContextMenuRequested.connect(self.showModelContextMenu)
309        self.lstParams.setAttribute(QtCore.Qt.WA_MacShowFocusRect, False)
310        # Poly model displayed in poly list
311        self.lstPoly.setModel(self._poly_model)
312        self.setPolyModel()
313        self.setTableProperties(self.lstPoly)
314        # Delegates for custom editing and display
315        self.lstPoly.setItemDelegate(PolyViewDelegate(self))
316        # Polydispersity function combo response
317        self.lstPoly.itemDelegate().combo_updated.connect(self.onPolyComboIndexChange)
318        self.lstPoly.itemDelegate().filename_updated.connect(self.onPolyFilenameChange)
319
320        # Magnetism model displayed in magnetism list
321        self.lstMagnetic.setModel(self._magnet_model)
322        self.setMagneticModel()
323        self.setTableProperties(self.lstMagnetic)
324        # Delegates for custom editing and display
325        self.lstMagnetic.setItemDelegate(MagnetismViewDelegate(self))
326
327    def initializeCategoryCombo(self):
328        """
329        Model category combo setup
330        """
331        category_list = sorted(self.master_category_dict.keys())
332        self.cbCategory.addItem(CATEGORY_DEFAULT)
333        self.cbCategory.addItems(category_list)
334        self.cbCategory.addItem(CATEGORY_STRUCTURE)
335        self.cbCategory.setCurrentIndex(0)
336
337    def setEnablementOnDataLoad(self):
338        """
339        Enable/disable various UI elements based on data loaded
340        """
341        # Tag along functionality
342        self.label.setText("Data loaded from: ")
343        self.lblFilename.setText(self.logic.data.filename)
344        self.updateQRange()
345        # Switch off Data2D control
346        self.chk2DView.setEnabled(False)
347        self.chk2DView.setVisible(False)
348        self.chkMagnetism.setEnabled(self.is2D)
349        self.tabFitting.setTabEnabled(TAB_MAGNETISM, self.is2D)
350        # Combo box or label for file name"
351        if self.is_batch_fitting:
352            self.lblFilename.setVisible(False)
353            for dataitem in self.all_data:
354                filename = GuiUtils.dataFromItem(dataitem).filename
355                self.cbFileNames.addItem(filename)
356            self.cbFileNames.setVisible(True)
357            self.chkChainFit.setEnabled(True)
358            self.chkChainFit.setVisible(True)
359            # This panel is not designed to view individual fits, so disable plotting
360            self.cmdPlot.setVisible(False)
361        # Similarly on other tabs
362        self.options_widget.setEnablementOnDataLoad()
363        self.onSelectModel()
364        # Smearing tab
365        self.smearing_widget.updateSmearing(self.data)
366
367    def acceptsData(self):
368        """ Tells the caller this widget can accept new dataset """
369        return not self.data_is_loaded
370
371    def disableModelCombo(self):
372        """ Disable the combobox """
373        self.cbModel.setEnabled(False)
374        self.lblModel.setEnabled(False)
375
376    def enableModelCombo(self):
377        """ Enable the combobox """
378        self.cbModel.setEnabled(True)
379        self.lblModel.setEnabled(True)
380
381    def disableStructureCombo(self):
382        """ Disable the combobox """
383        self.cbStructureFactor.setEnabled(False)
384        self.lblStructure.setEnabled(False)
385
386    def enableStructureCombo(self):
387        """ Enable the combobox """
388        self.cbStructureFactor.setEnabled(True)
389        self.lblStructure.setEnabled(True)
390
391    def togglePoly(self, isChecked):
392        """ Enable/disable the polydispersity tab """
393        self.tabFitting.setTabEnabled(TAB_POLY, isChecked)
394
395    def toggleMagnetism(self, isChecked):
396        """ Enable/disable the magnetism tab """
397        self.tabFitting.setTabEnabled(TAB_MAGNETISM, isChecked)
398
399    def toggleChainFit(self, isChecked):
400        """ Enable/disable chain fitting """
401        self.is_chain_fitting = isChecked
402
403    def toggle2D(self, isChecked):
404        """ Enable/disable the controls dependent on 1D/2D data instance """
405        self.chkMagnetism.setEnabled(isChecked)
406        self.is2D = isChecked
407        # Reload the current model
408        if self.kernel_module:
409            self.onSelectModel()
410
411    def initializeControls(self):
412        """
413        Set initial control enablement
414        """
415        self.cbFileNames.setVisible(False)
416        self.cmdFit.setEnabled(False)
417        self.cmdPlot.setEnabled(False)
418        self.options_widget.cmdComputePoints.setVisible(False) # probably redundant
419        self.chkPolydispersity.setEnabled(True)
420        self.chkPolydispersity.setCheckState(False)
421        self.chk2DView.setEnabled(True)
422        self.chk2DView.setCheckState(False)
423        self.chkMagnetism.setEnabled(False)
424        self.chkMagnetism.setCheckState(False)
425        self.chkChainFit.setEnabled(False)
426        self.chkChainFit.setVisible(False)
427        # Tabs
428        self.tabFitting.setTabEnabled(TAB_POLY, False)
429        self.tabFitting.setTabEnabled(TAB_MAGNETISM, False)
430        self.lblChi2Value.setText("---")
431        # Smearing tab
432        self.smearing_widget.updateSmearing(self.data)
433        # Line edits in the option tab
434        self.updateQRange()
435
436    def initializeSignals(self):
437        """
438        Connect GUI element signals
439        """
440        # Comboboxes
441        self.cbStructureFactor.currentIndexChanged.connect(self.onSelectStructureFactor)
442        self.cbCategory.currentIndexChanged.connect(self.onSelectCategory)
443        self.cbModel.currentIndexChanged.connect(self.onSelectModel)
444        self.cbFileNames.currentIndexChanged.connect(self.onSelectBatchFilename)
445        # Checkboxes
446        self.chk2DView.toggled.connect(self.toggle2D)
447        self.chkPolydispersity.toggled.connect(self.togglePoly)
448        self.chkMagnetism.toggled.connect(self.toggleMagnetism)
449        self.chkChainFit.toggled.connect(self.toggleChainFit)
450        # Buttons
451        self.cmdFit.clicked.connect(self.onFit)
452        self.cmdPlot.clicked.connect(self.onPlot)
453        self.cmdHelp.clicked.connect(self.onHelp)
454        self.cmdMagneticDisplay.clicked.connect(self.onDisplayMagneticAngles)
455
456        # Respond to change in parameters from the UI
457        self._model_model.itemChanged.connect(self.onMainParamsChange)
458        self._poly_model.itemChanged.connect(self.onPolyModelChange)
459        self._magnet_model.itemChanged.connect(self.onMagnetModelChange)
460
461        # Signals from separate tabs asking for replot
462        self.options_widget.plot_signal.connect(self.onOptionsUpdate)
463
464    def showModelContextMenu(self, position):
465
466        rows = len([s.row() for s in self.lstParams.selectionModel().selectedRows()])
467        menu = self.showModelDescription() if not rows else self.modelContextMenu(rows)
468        try:
469            menu.exec_(self.lstParams.viewport().mapToGlobal(position))
470        except AttributeError as ex:
471            logging.error("Error generating context menu: %s" % ex)
472        return
473
474    def modelContextMenu(self, rows):
475        menu = QtWidgets.QMenu()
476
477        # Select for fitting
478        self.actionSelect = QtWidgets.QAction(self)
479        self.actionSelect.setObjectName("actionSelect")
480        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select parameter for fitting"))
481        # Unselect from fitting
482        self.actionDeselect = QtWidgets.QAction(self)
483        self.actionDeselect.setObjectName("actionDeselect")
484        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select parameter from fitting"))
485
486        self.actionConstrain = QtWidgets.QAction(self)
487        self.actionConstrain.setObjectName("actionConstrain")
488        self.actionConstrain.setText(QtCore.QCoreApplication.translate("self", "Constrain parameter to current value"))
489
490        self.actionMultiConstrain = QtWidgets.QAction(self)
491        self.actionMultiConstrain.setObjectName("actionMultiConstrain")
492        self.actionMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Constrain selected parameters to their current values"))
493
494        self.actionMutualMultiConstrain = QtWidgets.QAction(self)
495        self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
496        self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of selected parameters..."))
497
498        #action.setDefaultWidget(label)
499        menu.addAction(self.actionSelect)
500        menu.addAction(self.actionDeselect)
501        menu.addSeparator()
502
503        if rows == 1:
504            menu.addAction(self.actionConstrain)
505        elif rows == 2:
506            menu.addAction(self.actionMultiConstrain)
507            menu.addAction(self.actionMutualMultiConstrain)
508        elif rows > 2:
509            menu.addAction(self.actionMultiConstrain)
510
511        # Define the callbacks
512        self.actionConstrain.triggered.connect(self.addConstraint)
513        self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstrain)
514        self.actionSelect.triggered.connect(self.selectParameters)
515        self.actionDeselect.triggered.connect(self.deselectParameters)
516        return menu
517
518    def showMultiConstrain(self):
519        """
520        Show the constraint widget and receive the expression
521        """
522        from sas.qtgui.Perspectives.Fitting.MultiConstraint import MultiConstraint
523        params_list = [s.data() for s in self.lstParams.selectionModel().selectedRows()]
524        mc_widget = MultiConstraint(self, params=params_list)
525        mc_widget.exec_()
526        constraint = mc_widget.txtConstraint.text()
527        # Pass the constraint to the parser
528        self.communicate.statusBarUpdateSignal.emit('Constraints added')
529        pass
530
531    def addConstraint(self):
532        """
533        Adds a constraint on a single parameter.
534        """
535        self.communicate.statusBarUpdateSignal.emit('Constraint added')
536        pass
537
538    def selectParameters(self):
539        """
540        Selected parameters are chosen for fitting
541        """
542        status = QtCore.Qt.Checked
543        self.setParameterSelection(status)
544
545    def deselectParameters(self):
546        """
547        Selected parameters are removed for fitting
548        """
549        status = QtCore.Qt.Unchecked
550        self.setParameterSelection(status)
551
552    def selectedParameters(self):
553        """ Returns list of selected (highlighted) parameters """
554        return [s.row() for s in self.lstParams.selectionModel().selectedRows() if self.isCheckable(s.row())]
555
556    def setParameterSelection(self, status=QtCore.Qt.Unchecked):
557        """
558        Selected parameters are chosen for fitting
559        """
560        # Convert to proper indices and set requested enablement
561        for row in self.selectedParameters():
562            self._model_model.item(row, 0).setCheckState(status)
563        pass
564
565    def showModelDescription(self):
566        """
567        Creates a window with model description, when right clicked in the treeview
568        """
569        msg = 'Model description:\n'
570        if self.kernel_module is not None:
571            if str(self.kernel_module.description).rstrip().lstrip() == '':
572                msg += "Sorry, no information is available for this model."
573            else:
574                msg += self.kernel_module.description + '\n'
575        else:
576            msg += "You must select a model to get information on this"
577
578        menu = QtWidgets.QMenu()
579        label = QtWidgets.QLabel(msg)
580        action = QtWidgets.QWidgetAction(self)
581        action.setDefaultWidget(label)
582        menu.addAction(action)
583        return menu
584
585    def onSelectModel(self):
586        """
587        Respond to select Model from list event
588        """
589        model = self.cbModel.currentText()
590
591        # empty combobox forced to be read
592        if not model:
593            return
594        # Reset structure factor
595        self.cbStructureFactor.setCurrentIndex(0)
596
597        # Reset parameters to fit
598        self.parameters_to_fit = None
599        self.has_error_column = False
600        self.has_poly_error_column = False
601
602        self.respondToModelStructure(model=model, structure_factor=None)
603
604    def onSelectBatchFilename(self, data_index):
605        """
606        Update the logic based on the selected file in batch fitting
607        """
608        self._index = self.all_data[data_index]
609        self.logic.data = GuiUtils.dataFromItem(self.all_data[data_index])
610        self.updateQRange()
611
612    def onSelectStructureFactor(self):
613        """
614        Select Structure Factor from list
615        """
616        model = str(self.cbModel.currentText())
617        category = str(self.cbCategory.currentText())
618        structure = str(self.cbStructureFactor.currentText())
619        if category == CATEGORY_STRUCTURE:
620            model = None
621        self.respondToModelStructure(model=model, structure_factor=structure)
622
623    def respondToModelStructure(self, model=None, structure_factor=None):
624        # Set enablement on calculate/plot
625        self.cmdPlot.setEnabled(True)
626
627        # kernel parameters -> model_model
628        self.SASModelToQModel(model, structure_factor)
629
630        if self.data_is_loaded:
631            self.cmdPlot.setText("Show Plot")
632            self.calculateQGridForModel()
633        else:
634            self.cmdPlot.setText("Calculate")
635            # Create default datasets if no data passed
636            self.createDefaultDataset()
637
638        # Update state stack
639        self.updateUndo()
640
641    def onSelectCategory(self):
642        """
643        Select Category from list
644        """
645        category = self.cbCategory.currentText()
646        # Check if the user chose "Choose category entry"
647        if category == CATEGORY_DEFAULT:
648            # if the previous category was not the default, keep it.
649            # Otherwise, just return
650            if self._previous_category_index != 0:
651                # We need to block signals, or else state changes on perceived unchanged conditions
652                self.cbCategory.blockSignals(True)
653                self.cbCategory.setCurrentIndex(self._previous_category_index)
654                self.cbCategory.blockSignals(False)
655            return
656
657        if category == CATEGORY_STRUCTURE:
658            self.disableModelCombo()
659            self.enableStructureCombo()
660            self._model_model.clear()
661            return
662
663        # Safely clear and enable the model combo
664        self.cbModel.blockSignals(True)
665        self.cbModel.clear()
666        self.cbModel.blockSignals(False)
667        self.enableModelCombo()
668        self.disableStructureCombo()
669
670        self._previous_category_index = self.cbCategory.currentIndex()
671        # Retrieve the list of models
672        model_list = self.master_category_dict[category]
673        # Populate the models combobox
674        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
675
676    def onPolyModelChange(self, item):
677        """
678        Callback method for updating the main model and sasmodel
679        parameters with the GUI values in the polydispersity view
680        """
681        model_column = item.column()
682        model_row = item.row()
683        name_index = self._poly_model.index(model_row, 0)
684        parameter_name = str(name_index.data()).lower() # "distribution of sld" etc.
685        if "distribution of" in parameter_name:
686            # just the last word
687            parameter_name = parameter_name.rsplit()[-1]
688
689        # Extract changed value.
690        if model_column == self.lstPoly.itemDelegate().poly_parameter:
691            # Is the parameter checked for fitting?
692            value = item.checkState()
693            parameter_name = parameter_name + '.width'
694            if value == QtCore.Qt.Checked:
695                self.parameters_to_fit.append(parameter_name)
696            else:
697                if parameter_name in self.parameters_to_fit:
698                    self.parameters_to_fit.remove(parameter_name)
699            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
700            return
701        elif model_column in [self.lstPoly.itemDelegate().poly_min, self.lstPoly.itemDelegate().poly_max]:
702            try:
703                value = GuiUtils.toDouble(item.text())
704            except TypeError:
705                # Can't be converted properly, bring back the old value and exit
706                return
707
708            current_details = self.kernel_module.details[parameter_name]
709            current_details[model_column-1] = value
710        elif model_column == self.lstPoly.itemDelegate().poly_function:
711            # name of the function - just pass
712            return
713        elif model_column == self.lstPoly.itemDelegate().poly_filename:
714            # filename for array - just pass
715            return
716        else:
717            try:
718                value = GuiUtils.toDouble(item.text())
719            except TypeError:
720                # Can't be converted properly, bring back the old value and exit
721                return
722
723            # Update the sasmodel
724            # PD[ratio] -> width, npts -> npts, nsigs -> nsigmas
725            self.kernel_module.setParam(parameter_name + '.' + \
726                                        self.lstPoly.itemDelegate().columnDict()[model_column], value)
727
728    def onMagnetModelChange(self, item):
729        """
730        Callback method for updating the sasmodel magnetic parameters with the GUI values
731        """
732        model_column = item.column()
733        model_row = item.row()
734        name_index = self._magnet_model.index(model_row, 0)
735        parameter_name = str(self._magnet_model.data(name_index))
736
737        if model_column == 0:
738            value = item.checkState()
739            if value == QtCore.Qt.Checked:
740                self.parameters_to_fit.append(parameter_name)
741            else:
742                if parameter_name in self.parameters_to_fit:
743                    self.parameters_to_fit.remove(parameter_name)
744            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
745            # Update state stack
746            self.updateUndo()
747            return
748
749        # Extract changed value.
750        try:
751            value = GuiUtils.toDouble(item.text())
752        except TypeError:
753            # Unparsable field
754            return
755
756        property_index = self._magnet_model.headerData(1, model_column)-1 # Value, min, max, etc.
757
758        # Update the parameter value - note: this supports +/-inf as well
759        self.kernel_module.params[parameter_name] = value
760
761        # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
762        self.kernel_module.details[parameter_name][property_index] = value
763
764        # Force the chart update when actual parameters changed
765        if model_column == 1:
766            self.recalculatePlotData()
767
768        # Update state stack
769        self.updateUndo()
770
771    def onHelp(self):
772        """
773        Show the "Fitting" section of help
774        """
775        tree_location = GuiUtils.HELP_DIRECTORY_LOCATION + "/user/sasgui/perspectives/fitting/"
776
777        # Actual file will depend on the current tab
778        tab_id = self.tabFitting.currentIndex()
779        helpfile = "fitting.html"
780        if tab_id == 0:
781            helpfile = "fitting_help.html"
782        elif tab_id == 1:
783            helpfile = "residuals_help.html"
784        elif tab_id == 2:
785            helpfile = "sm_help.html"
786        elif tab_id == 3:
787            helpfile = "pd_help.html"
788        elif tab_id == 4:
789            helpfile = "mag_help.html"
790        help_location = tree_location + helpfile
791
792        content = QtCore.QUrl(help_location)
793        self.helpView.load(QtCore.QUrl(help_location))
794        self.helpView.show()
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.