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

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

More functionality for single model constraints SASVIEW-843

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