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

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

Fixed issue with unwanted headers

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