source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 0595bb7

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

Constraint validator - initial implementation + tests. SASVIEW-844

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