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

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

Code review fixes for FittingWidget?

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