source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 6b0c2f6

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 6b0c2f6 was 6b0c2f6, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Constraint validator - initial implementation + tests. SASVIEW-844

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