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

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

Add model name to constraints on the same model

  • Property mode set to 100644
File size: 88.9 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        model_name = self.kernel_module.name
577        constraint.func = model_name + "." + c_text
578        # Which row is the constrained parameter in?
579        row = self.getRowFromName(constraint.param)
580
581        # Create a new item and add the Constraint object as a child
582        self.addConstraintToRow(constraint=constraint, row=row)
583
584    def getRowFromName(self, name):
585        """
586        Given parameter name get the row number in self._model_model
587        """
588        for row in range(self._model_model.rowCount()):
589            row_name = self._model_model.item(row).text()
590            if row_name == name:
591                return row
592        return None
593
594    def getParamNames(self):
595        """
596        Return list of all parameters for the current model
597        """
598        return [self._model_model.item(row).text() for row in range(self._model_model.rowCount())]
599
600    def modifyViewOnRow(self, row, font=None, brush=None):
601        """
602        Chage how the given row of the main model is shown
603        """
604        fields_enabled = False
605        if font is None:
606            font = QtGui.QFont()
607            fields_enabled = True
608        if brush is None:
609            brush = QtGui.QBrush()
610            fields_enabled = True
611        self._model_model.blockSignals(True)
612        # Modify font and foreground of affected rows
613        for column in range(0, self._model_model.columnCount()):
614            self._model_model.item(row, column).setForeground(brush)
615            self._model_model.item(row, column).setFont(font)
616            self._model_model.item(row, column).setEditable(fields_enabled)
617        self._model_model.blockSignals(False)
618
619    def addConstraintToRow(self, constraint=None, row=0):
620        """
621        Adds the constraint object to requested row
622        """
623        # Create a new item and add the Constraint object as a child
624        assert(isinstance(constraint, Constraint))
625        assert(0<=row<=self._model_model.rowCount())
626
627        item = QtGui.QStandardItem()
628        item.setData(constraint)
629        self._model_model.item(row, 1).setChild(0, item)
630        # Set min/max to the value constrained
631        self.constraintAddedSignal.emit([row])
632        # Show visual hints for the constraint
633        font = QtGui.QFont()
634        font.setItalic(True)
635        brush = QtGui.QBrush(QtGui.QColor('blue'))
636        self.modifyViewOnRow(row, font=font, brush=brush)
637        self.communicate.statusBarUpdateSignal.emit('Constraint added')
638
639    def addSimpleConstraint(self):
640        """
641        Adds a constraint on a single parameter.
642        """
643        min_col = self.lstParams.itemDelegate().param_min
644        max_col = self.lstParams.itemDelegate().param_max
645        for row in self.selectedParameters():
646            param = self._model_model.item(row, 0).text()
647            value = self._model_model.item(row, 1).text()
648            min_t = self._model_model.item(row, min_col).text()
649            max_t = self._model_model.item(row, max_col).text()
650            # Create a Constraint object
651            constraint = Constraint(param=param, value=value, min=min_t, max=max_t)
652            # Create a new item and add the Constraint object as a child
653            item = QtGui.QStandardItem()
654            item.setData(constraint)
655            self._model_model.item(row, 1).setChild(0, item)
656            # Assumed correctness from the validator
657            value = float(value)
658            # BUMPS calculates log(max-min) without any checks, so let's assign minor range
659            min_v = value - (value/10000.0)
660            max_v = value + (value/10000.0)
661            # Set min/max to the value constrained
662            self._model_model.item(row, min_col).setText(str(min_v))
663            self._model_model.item(row, max_col).setText(str(max_v))
664            self.constraintAddedSignal.emit([row])
665            # Show visual hints for the constraint
666            font = QtGui.QFont()
667            font.setItalic(True)
668            brush = QtGui.QBrush(QtGui.QColor('blue'))
669            self.modifyViewOnRow(row, font=font, brush=brush)
670        self.communicate.statusBarUpdateSignal.emit('Constraint added')
671
672    def deleteConstraint(self):
673        """
674        Delete constraints from selected parameters.
675        """
676        self.deleteConstraintOnParameter(param=None)
677
678    def deleteConstraintOnParameter(self, param=None):
679        """
680        Delete the constraint on model parameter 'param'
681        """
682        min_col = self.lstParams.itemDelegate().param_min
683        max_col = self.lstParams.itemDelegate().param_max
684        for row in range(self._model_model.rowCount()):
685            # Get the Constraint object from of the model item
686            item = self._model_model.item(row, 1)
687            if not item.hasChildren():
688                continue
689            constraint = item.child(0).data()
690            if constraint is None:
691                continue
692            if not isinstance(constraint, Constraint):
693                continue
694            if param and constraint.param != param:
695                continue
696            # Now we got the right row. Delete the constraint and clean up
697            # Retrieve old values and put them on the model
698            if constraint.min is not None:
699                self._model_model.item(row, min_col).setText(constraint.min)
700            if constraint.max is not None:
701                self._model_model.item(row, max_col).setText(constraint.max)
702            # Remove constraint item
703            item.removeRow(0)
704            self.constraintAddedSignal.emit([row])
705            self.modifyViewOnRow(row)
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*M2.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        if assert in None:
1161            msg = "Fitting failed after: %s s.\n" % GuiUtils.formatNumber(elapsed)
1162            self.communicate.statusBarUpdateSignal.emit(msg)
1163            return
1164
1165        res_list = result[0][0]
1166        res = res_list[0]
1167        if res.fitness is None or \
1168            not np.isfinite(res.fitness) or \
1169            np.any(res.pvec is None) or \
1170            not np.all(np.isfinite(res.pvec)):
1171            msg = "Fitting did not converge!"
1172            self.communicate.statusBarUpdateSignal.emit(msg)
1173            logging.error(msg)
1174            return
1175
1176        elapsed = result[1]
1177        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
1178        self.communicate.statusBarUpdateSignal.emit(msg)
1179
1180        self.chi2 = res.fitness
1181        param_list = res.param_list # ['radius', 'radius.width']
1182        param_values = res.pvec     # array([ 0.36221662,  0.0146783 ])
1183        param_stderr = res.stderr   # array([ 1.71293015,  1.71294233])
1184        params_and_errors = list(zip(param_values, param_stderr))
1185        param_dict = dict(zip(param_list, params_and_errors))
1186
1187        # Dictionary of fitted parameter: value, error
1188        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
1189        self.updateModelFromList(param_dict)
1190
1191        self.updatePolyModelFromList(param_dict)
1192
1193        self.updateMagnetModelFromList(param_dict)
1194
1195        # update charts
1196        self.onPlot()
1197
1198        # Read only value - we can get away by just printing it here
1199        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
1200        self.lblChi2Value.setText(chi2_repr)
1201
1202    def prepareFitters(self, fitter=None, fit_id=0):
1203        """
1204        Prepare the Fitter object for use in fitting
1205        """
1206        # fitter = None -> single/batch fitting
1207        # fitter = Fit() -> simultaneous fitting
1208
1209        # Data going in
1210        data = self.logic.data
1211        model = self.kernel_module
1212        qmin = self.q_range_min
1213        qmax = self.q_range_max
1214        params_to_fit = self.parameters_to_fit
1215        if (not params_to_fit):
1216            raise ValueError('Fitting requires at least one parameter to optimize.')
1217
1218        # Potential weights added directly to data
1219        self.addWeightingToData(data)
1220
1221        # Potential smearing added
1222        # Remember that smearing_min/max can be None ->
1223        # deal with it until Python gets discriminated unions
1224        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
1225
1226        constraints = self.getComplexConstraintsForModel()
1227        smearer = None
1228        handler = None
1229        batch_inputs = {}
1230        batch_outputs = {}
1231
1232        fitters = []
1233        for fit_index in self.all_data:
1234            fitter_single = Fit() if fitter is None else fitter
1235            data = GuiUtils.dataFromItem(fit_index)
1236            try:
1237                fitter_single.set_model(model, fit_id, params_to_fit, data=data,
1238                             constraints=constraints)
1239            except ValueError as ex:
1240                logging.error("Setting model parameters failed with: %s" % ex)
1241                return
1242
1243            qmin, qmax, _ = self.logic.computeRangeFromData(data)
1244            fitter_single.set_data(data=data, id=fit_id, smearer=smearer, qmin=qmin,
1245                            qmax=qmax)
1246            fitter_single.select_problem_for_fit(id=fit_id, value=1)
1247            if fitter is None:
1248                # Assign id to the new fitter only
1249                fitter_single.fitter_id = [self.page_id]
1250            fit_id += 1
1251            fitters.append(fitter_single)
1252
1253        return fitters, fit_id
1254
1255    def iterateOverModel(self, func):
1256        """
1257        Take func and throw it inside the model row loop
1258        """
1259        for row_i in range(self._model_model.rowCount()):
1260            func(row_i)
1261
1262    def updateModelFromList(self, param_dict):
1263        """
1264        Update the model with new parameters, create the errors column
1265        """
1266        assert isinstance(param_dict, dict)
1267        if not dict:
1268            return
1269
1270        def updateFittedValues(row):
1271            # Utility function for main model update
1272            # internal so can use closure for param_dict
1273            param_name = str(self._model_model.item(row, 0).text())
1274            if param_name not in list(param_dict.keys()):
1275                return
1276            # modify the param value
1277            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1278            self._model_model.item(row, 1).setText(param_repr)
1279            if self.has_error_column:
1280                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1281                self._model_model.item(row, 2).setText(error_repr)
1282
1283        def updatePolyValues(row):
1284            # Utility function for updateof polydispersity part of the main model
1285            param_name = str(self._model_model.item(row, 0).text())+'.width'
1286            if param_name not in list(param_dict.keys()):
1287                return
1288            # modify the param value
1289            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1290            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
1291
1292        def createErrorColumn(row):
1293            # Utility function for error column update
1294            item = QtGui.QStandardItem()
1295            def createItem(param_name):
1296                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1297                item.setText(error_repr)
1298            def curr_param():
1299                return str(self._model_model.item(row, 0).text())
1300
1301            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1302
1303            error_column.append(item)
1304
1305        # block signals temporarily, so we don't end up
1306        # updating charts with every single model change on the end of fitting
1307        self._model_model.blockSignals(True)
1308        self.iterateOverModel(updateFittedValues)
1309        self.iterateOverModel(updatePolyValues)
1310        self._model_model.blockSignals(False)
1311
1312        if self.has_error_column:
1313            return
1314
1315        error_column = []
1316        self.lstParams.itemDelegate().addErrorColumn()
1317        self.iterateOverModel(createErrorColumn)
1318
1319        # switch off reponse to model change
1320        self._model_model.blockSignals(True)
1321        self._model_model.insertColumn(2, error_column)
1322        self._model_model.blockSignals(False)
1323        FittingUtilities.addErrorHeadersToModel(self._model_model)
1324        # Adjust the table cells width.
1325        # TODO: find a way to dynamically adjust column width while resized expanding
1326        self.lstParams.resizeColumnToContents(0)
1327        self.lstParams.resizeColumnToContents(4)
1328        self.lstParams.resizeColumnToContents(5)
1329        self.lstParams.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
1330
1331        self.has_error_column = True
1332
1333    def updatePolyModelFromList(self, param_dict):
1334        """
1335        Update the polydispersity model with new parameters, create the errors column
1336        """
1337        assert isinstance(param_dict, dict)
1338        if not dict:
1339            return
1340
1341        def iterateOverPolyModel(func):
1342            """
1343            Take func and throw it inside the poly model row loop
1344            """
1345            for row_i in range(self._poly_model.rowCount()):
1346                func(row_i)
1347
1348        def updateFittedValues(row_i):
1349            # Utility function for main model update
1350            # internal so can use closure for param_dict
1351            if row_i >= self._poly_model.rowCount():
1352                return
1353            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1354            if param_name not in list(param_dict.keys()):
1355                return
1356            # modify the param value
1357            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1358            self._poly_model.item(row_i, 1).setText(param_repr)
1359            if self.has_poly_error_column:
1360                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1361                self._poly_model.item(row_i, 2).setText(error_repr)
1362
1363
1364        def createErrorColumn(row_i):
1365            # Utility function for error column update
1366            if row_i >= self._poly_model.rowCount():
1367                return
1368            item = QtGui.QStandardItem()
1369
1370            def createItem(param_name):
1371                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1372                item.setText(error_repr)
1373
1374            def poly_param():
1375                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1376
1377            [createItem(param_name) for param_name in list(param_dict.keys()) if poly_param() == param_name]
1378
1379            error_column.append(item)
1380
1381        # block signals temporarily, so we don't end up
1382        # updating charts with every single model change on the end of fitting
1383        self._poly_model.blockSignals(True)
1384        iterateOverPolyModel(updateFittedValues)
1385        self._poly_model.blockSignals(False)
1386
1387        if self.has_poly_error_column:
1388            return
1389
1390        self.lstPoly.itemDelegate().addErrorColumn()
1391        error_column = []
1392        iterateOverPolyModel(createErrorColumn)
1393
1394        # switch off reponse to model change
1395        self._poly_model.blockSignals(True)
1396        self._poly_model.insertColumn(2, error_column)
1397        self._poly_model.blockSignals(False)
1398        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
1399
1400        self.has_poly_error_column = True
1401
1402    def updateMagnetModelFromList(self, param_dict):
1403        """
1404        Update the magnetic model with new parameters, create the errors column
1405        """
1406        assert isinstance(param_dict, dict)
1407        if not dict:
1408            return
1409        if self._model_model.rowCount() == 0:
1410            return
1411
1412        def iterateOverMagnetModel(func):
1413            """
1414            Take func and throw it inside the magnet model row loop
1415            """
1416            for row_i in range(self._model_model.rowCount()):
1417                func(row_i)
1418
1419        def updateFittedValues(row):
1420            # Utility function for main model update
1421            # internal so can use closure for param_dict
1422            if self._magnet_model.item(row, 0) is None:
1423                return
1424            param_name = str(self._magnet_model.item(row, 0).text())
1425            if param_name not in list(param_dict.keys()):
1426                return
1427            # modify the param value
1428            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1429            self._magnet_model.item(row, 1).setText(param_repr)
1430            if self.has_magnet_error_column:
1431                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1432                self._magnet_model.item(row, 2).setText(error_repr)
1433
1434        def createErrorColumn(row):
1435            # Utility function for error column update
1436            item = QtGui.QStandardItem()
1437            def createItem(param_name):
1438                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1439                item.setText(error_repr)
1440            def curr_param():
1441                return str(self._magnet_model.item(row, 0).text())
1442
1443            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
1444
1445            error_column.append(item)
1446
1447        # block signals temporarily, so we don't end up
1448        # updating charts with every single model change on the end of fitting
1449        self._magnet_model.blockSignals(True)
1450        iterateOverMagnetModel(updateFittedValues)
1451        self._magnet_model.blockSignals(False)
1452
1453        if self.has_magnet_error_column:
1454            return
1455
1456        self.lstMagnetic.itemDelegate().addErrorColumn()
1457        error_column = []
1458        iterateOverMagnetModel(createErrorColumn)
1459
1460        # switch off reponse to model change
1461        self._magnet_model.blockSignals(True)
1462        self._magnet_model.insertColumn(2, error_column)
1463        self._magnet_model.blockSignals(False)
1464        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1465
1466        self.has_magnet_error_column = True
1467
1468    def onPlot(self):
1469        """
1470        Plot the current set of data
1471        """
1472        # Regardless of previous state, this should now be `plot show` functionality only
1473        self.cmdPlot.setText("Show Plot")
1474        # Force data recalculation so existing charts are updated
1475        self.recalculatePlotData()
1476        self.showPlot()
1477
1478    def recalculatePlotData(self):
1479        """
1480        Generate a new dataset for model
1481        """
1482        if not self.data_is_loaded:
1483            self.createDefaultDataset()
1484        self.calculateQGridForModel()
1485
1486    def showPlot(self):
1487        """
1488        Show the current plot in MPL
1489        """
1490        # Show the chart if ready
1491        data_to_show = self.data if self.data_is_loaded else self.model_data
1492        if data_to_show is not None:
1493            self.communicate.plotRequestedSignal.emit([data_to_show])
1494
1495    def onOptionsUpdate(self):
1496        """
1497        Update local option values and replot
1498        """
1499        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1500            self.options_widget.state()
1501        # set Q range labels on the main tab
1502        self.lblMinRangeDef.setText(str(self.q_range_min))
1503        self.lblMaxRangeDef.setText(str(self.q_range_max))
1504        self.recalculatePlotData()
1505
1506    def setDefaultStructureCombo(self):
1507        """
1508        Fill in the structure factors combo box with defaults
1509        """
1510        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1511        factors = [factor[0] for factor in structure_factor_list]
1512        factors.insert(0, STRUCTURE_DEFAULT)
1513        self.cbStructureFactor.clear()
1514        self.cbStructureFactor.addItems(sorted(factors))
1515
1516    def createDefaultDataset(self):
1517        """
1518        Generate default Dataset 1D/2D for the given model
1519        """
1520        # Create default datasets if no data passed
1521        if self.is2D:
1522            qmax = self.q_range_max/np.sqrt(2)
1523            qstep = self.npts
1524            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
1525            return
1526        elif self.log_points:
1527            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
1528            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
1529            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
1530        else:
1531            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
1532                                   num=self.npts, endpoint=True)
1533        self.logic.createDefault1dData(interval, self.tab_id)
1534
1535    def readCategoryInfo(self):
1536        """
1537        Reads the categories in from file
1538        """
1539        self.master_category_dict = defaultdict(list)
1540        self.by_model_dict = defaultdict(list)
1541        self.model_enabled_dict = defaultdict(bool)
1542
1543        categorization_file = CategoryInstaller.get_user_file()
1544        if not os.path.isfile(categorization_file):
1545            categorization_file = CategoryInstaller.get_default_file()
1546        with open(categorization_file, 'rb') as cat_file:
1547            self.master_category_dict = json.load(cat_file)
1548            self.regenerateModelDict()
1549
1550        # Load the model dict
1551        models = load_standard_models()
1552        for model in models:
1553            self.models[model.name] = model
1554
1555    def regenerateModelDict(self):
1556        """
1557        Regenerates self.by_model_dict which has each model name as the
1558        key and the list of categories belonging to that model
1559        along with the enabled mapping
1560        """
1561        self.by_model_dict = defaultdict(list)
1562        for category in self.master_category_dict:
1563            for (model, enabled) in self.master_category_dict[category]:
1564                self.by_model_dict[model].append(category)
1565                self.model_enabled_dict[model] = enabled
1566
1567    def addBackgroundToModel(self, model):
1568        """
1569        Adds background parameter with default values to the model
1570        """
1571        assert isinstance(model, QtGui.QStandardItemModel)
1572        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
1573        FittingUtilities.addCheckedListToModel(model, checked_list)
1574        last_row = model.rowCount()-1
1575        model.item(last_row, 0).setEditable(False)
1576        model.item(last_row, 4).setEditable(False)
1577
1578    def addScaleToModel(self, model):
1579        """
1580        Adds scale parameter with default values to the model
1581        """
1582        assert isinstance(model, QtGui.QStandardItemModel)
1583        checked_list = ['scale', '1.0', '0.0', 'inf', '']
1584        FittingUtilities.addCheckedListToModel(model, checked_list)
1585        last_row = model.rowCount()-1
1586        model.item(last_row, 0).setEditable(False)
1587        model.item(last_row, 4).setEditable(False)
1588
1589    def addWeightingToData(self, data):
1590        """
1591        Adds weighting contribution to fitting data
1592        """
1593        # Send original data for weighting
1594        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
1595        update_module = data.err_data if self.is2D else data.dy
1596        # Overwrite relevant values in data
1597        update_module = weight
1598
1599    def updateQRange(self):
1600        """
1601        Updates Q Range display
1602        """
1603        if self.data_is_loaded:
1604            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
1605        # set Q range labels on the main tab
1606        self.lblMinRangeDef.setText(str(self.q_range_min))
1607        self.lblMaxRangeDef.setText(str(self.q_range_max))
1608        # set Q range labels on the options tab
1609        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
1610
1611    def SASModelToQModel(self, model_name, structure_factor=None):
1612        """
1613        Setting model parameters into table based on selected category
1614        """
1615        # Crete/overwrite model items
1616        self._model_model.clear()
1617
1618        # First, add parameters from the main model
1619        if model_name is not None:
1620            self.fromModelToQModel(model_name)
1621
1622        # Then, add structure factor derived parameters
1623        if structure_factor is not None and structure_factor != "None":
1624            if model_name is None:
1625                # Instantiate the current sasmodel for SF-only models
1626                self.kernel_module = self.models[structure_factor]()
1627            self.fromStructureFactorToQModel(structure_factor)
1628        else:
1629            # Allow the SF combobox visibility for the given sasmodel
1630            self.enableStructureFactorControl(structure_factor)
1631
1632        # Then, add multishells
1633        if model_name is not None:
1634            # Multishell models need additional treatment
1635            self.addExtraShells()
1636
1637        # Add polydispersity to the model
1638        self.setPolyModel()
1639        # Add magnetic parameters to the model
1640        self.setMagneticModel()
1641
1642        # Adjust the table cells width
1643        self.lstParams.resizeColumnToContents(0)
1644        self.lstParams.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
1645
1646        # Now we claim the model has been loaded
1647        self.model_is_loaded = True
1648        # Change the model name to a monicker
1649        self.kernel_module.name = self.modelName()
1650
1651        # (Re)-create headers
1652        FittingUtilities.addHeadersToModel(self._model_model)
1653        self.lstParams.header().setFont(self.boldFont)
1654
1655        # Update Q Ranges
1656        self.updateQRange()
1657
1658    def fromModelToQModel(self, model_name):
1659        """
1660        Setting model parameters into QStandardItemModel based on selected _model_
1661        """
1662        kernel_module = generate.load_kernel_module(model_name)
1663        self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
1664
1665        # Instantiate the current sasmodel
1666        self.kernel_module = self.models[model_name]()
1667
1668        # Explicitly add scale and background with default values
1669        temp_undo_state = self.undo_supported
1670        self.undo_supported = False
1671        self.addScaleToModel(self._model_model)
1672        self.addBackgroundToModel(self._model_model)
1673        self.undo_supported = temp_undo_state
1674
1675        self.shell_names = self.shellNamesList()
1676
1677        # Update the QModel
1678        new_rows = FittingUtilities.addParametersToModel(self.model_parameters, self.kernel_module, self.is2D)
1679
1680        for row in new_rows:
1681            self._model_model.appendRow(row)
1682        # Update the counter used for multishell display
1683        self._last_model_row = self._model_model.rowCount()
1684
1685    def fromStructureFactorToQModel(self, structure_factor):
1686        """
1687        Setting model parameters into QStandardItemModel based on selected _structure factor_
1688        """
1689        structure_module = generate.load_kernel_module(structure_factor)
1690        structure_parameters = modelinfo.make_parameter_table(getattr(structure_module, 'parameters', []))
1691        structure_kernel = self.models[structure_factor]()
1692
1693        self.kernel_module._model_info = product.make_product_info(self.kernel_module._model_info, structure_kernel._model_info)
1694
1695        new_rows = FittingUtilities.addSimpleParametersToModel(structure_parameters, self.is2D)
1696        for row in new_rows:
1697            self._model_model.appendRow(row)
1698        # Update the counter used for multishell display
1699        self._last_model_row = self._model_model.rowCount()
1700
1701    def onMainParamsChange(self, item):
1702        """
1703        Callback method for updating the sasmodel parameters with the GUI values
1704        """
1705        model_column = item.column()
1706
1707        if model_column == 0:
1708            self.checkboxSelected(item)
1709            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
1710            # Update state stack
1711            self.updateUndo()
1712            return
1713
1714        model_row = item.row()
1715        name_index = self._model_model.index(model_row, 0)
1716
1717        # Extract changed value.
1718        try:
1719            value = GuiUtils.toDouble(item.text())
1720        except TypeError:
1721            # Unparsable field
1722            return
1723
1724        parameter_name = str(self._model_model.data(name_index)) # sld, background etc.
1725
1726        # Update the parameter value - note: this supports +/-inf as well
1727        self.kernel_module.params[parameter_name] = value
1728
1729        # Update the parameter value - note: this supports +/-inf as well
1730        param_column = self.lstParams.itemDelegate().param_value
1731        min_column = self.lstParams.itemDelegate().param_min
1732        max_column = self.lstParams.itemDelegate().param_max
1733        if model_column == param_column:
1734            self.kernel_module.setParam(parameter_name, value)
1735        elif model_column == min_column:
1736            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1737            self.kernel_module.details[parameter_name][1] = value
1738        elif model_column == max_column:
1739            self.kernel_module.details[parameter_name][2] = value
1740        else:
1741            # don't update the chart
1742            return
1743
1744        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
1745        # TODO: multishell params in self.kernel_module.details[??] = value
1746
1747        # Force the chart update when actual parameters changed
1748        if model_column == 1:
1749            self.recalculatePlotData()
1750
1751        # Update state stack
1752        self.updateUndo()
1753
1754    def isCheckable(self, row):
1755        return self._model_model.item(row, 0).isCheckable()
1756
1757    def checkboxSelected(self, item):
1758        # Assure we're dealing with checkboxes
1759        if not item.isCheckable():
1760            return
1761        status = item.checkState()
1762
1763        # If multiple rows selected - toggle all of them, filtering uncheckable
1764        # Switch off signaling from the model to avoid recursion
1765        self._model_model.blockSignals(True)
1766        # Convert to proper indices and set requested enablement
1767        self.setParameterSelection(status)
1768        #[self._model_model.item(row, 0).setCheckState(status) for row in self.selectedParameters()]
1769        self._model_model.blockSignals(False)
1770
1771        # update the list of parameters to fit
1772        main_params = self.checkedListFromModel(self._model_model)
1773        poly_params = self.checkedListFromModel(self._poly_model)
1774        magnet_params = self.checkedListFromModel(self._magnet_model)
1775
1776        # Retrieve poly params names
1777        poly_params = [param.rsplit()[-1] + '.width' for param in poly_params]
1778
1779        self.parameters_to_fit = main_params + poly_params + magnet_params
1780
1781    def checkedListFromModel(self, model):
1782        """
1783        Returns list of checked parameters for given model
1784        """
1785        def isChecked(row):
1786            return model.item(row, 0).checkState() == QtCore.Qt.Checked
1787
1788        return [str(model.item(row_index, 0).text())
1789                for row_index in range(model.rowCount())
1790                if isChecked(row_index)]
1791
1792    def createNewIndex(self, fitted_data):
1793        """
1794        Create a model or theory index with passed Data1D/Data2D
1795        """
1796        if self.data_is_loaded:
1797            if not fitted_data.name:
1798                name = self.nameForFittedData(self.data.filename)
1799                fitted_data.title = name
1800                fitted_data.name = name
1801                fitted_data.filename = name
1802                fitted_data.symbol = "Line"
1803            self.updateModelIndex(fitted_data)
1804        else:
1805            name = self.nameForFittedData(self.kernel_module.name)
1806            fitted_data.title = name
1807            fitted_data.name = name
1808            fitted_data.filename = name
1809            fitted_data.symbol = "Line"
1810            self.createTheoryIndex(fitted_data)
1811
1812    def updateModelIndex(self, fitted_data):
1813        """
1814        Update a QStandardModelIndex containing model data
1815        """
1816        name = self.nameFromData(fitted_data)
1817        # Make this a line if no other defined
1818        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
1819            fitted_data.symbol = 'Line'
1820        # Notify the GUI manager so it can update the main model in DataExplorer
1821        GuiUtils.updateModelItemWithPlot(self._index, fitted_data, name)
1822
1823    def createTheoryIndex(self, fitted_data):
1824        """
1825        Create a QStandardModelIndex containing model data
1826        """
1827        name = self.nameFromData(fitted_data)
1828        # Notify the GUI manager so it can create the theory model in DataExplorer
1829        new_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
1830        self.communicate.updateTheoryFromPerspectiveSignal.emit(new_item)
1831
1832    def nameFromData(self, fitted_data):
1833        """
1834        Return name for the dataset. Terribly impure function.
1835        """
1836        if fitted_data.name is None:
1837            name = self.nameForFittedData(self.logic.data.filename)
1838            fitted_data.title = name
1839            fitted_data.name = name
1840            fitted_data.filename = name
1841        else:
1842            name = fitted_data.name
1843        return name
1844
1845    def methodCalculateForData(self):
1846        '''return the method for data calculation'''
1847        return Calc1D if isinstance(self.data, Data1D) else Calc2D
1848
1849    def methodCompleteForData(self):
1850        '''return the method for result parsin on calc complete '''
1851        return self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1852
1853    def calculateQGridForModel(self):
1854        """
1855        Prepare the fitting data object, based on current ModelModel
1856        """
1857        if self.kernel_module is None:
1858            return
1859        # Awful API to a backend method.
1860        method = self.methodCalculateForData()(data=self.data,
1861                                               model=self.kernel_module,
1862                                               page_id=0,
1863                                               qmin=self.q_range_min,
1864                                               qmax=self.q_range_max,
1865                                               smearer=None,
1866                                               state=None,
1867                                               weight=None,
1868                                               fid=None,
1869                                               toggle_mode_on=False,
1870                                               completefn=None,
1871                                               update_chisqr=True,
1872                                               exception_handler=self.calcException,
1873                                               source=None)
1874
1875        calc_thread = threads.deferToThread(method.compute)
1876        calc_thread.addCallback(self.methodCompleteForData())
1877        calc_thread.addErrback(self.calculateDataFailed)
1878
1879    def calculateDataFailed(self, reason):
1880        """
1881        Thread returned error
1882        """
1883        print("Calculate Data failed with ", reason)
1884
1885    def complete1D(self, return_data):
1886        """
1887        Plot the current 1D data
1888        """
1889        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
1890        self.calculateResiduals(fitted_data)
1891        self.model_data = fitted_data
1892
1893    def complete2D(self, return_data):
1894        """
1895        Plot the current 2D data
1896        """
1897        fitted_data = self.logic.new2DPlot(return_data)
1898        self.calculateResiduals(fitted_data)
1899        self.model_data = fitted_data
1900
1901    def calculateResiduals(self, fitted_data):
1902        """
1903        Calculate and print Chi2 and display chart of residuals
1904        """
1905        # Create a new index for holding data
1906        fitted_data.symbol = "Line"
1907
1908        # Modify fitted_data with weighting
1909        self.addWeightingToData(fitted_data)
1910
1911        self.createNewIndex(fitted_data)
1912        # Calculate difference between return_data and logic.data
1913        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
1914        # Update the control
1915        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
1916        self.lblChi2Value.setText(chi2_repr)
1917
1918        self.communicate.plotUpdateSignal.emit([fitted_data])
1919
1920        # Plot residuals if actual data
1921        if not self.data_is_loaded:
1922            return
1923
1924        residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
1925        residuals_plot.id = "Residual " + residuals_plot.id
1926        self.createNewIndex(residuals_plot)
1927        #self.communicate.plotUpdateSignal.emit([residuals_plot])
1928
1929    def calcException(self, etype, value, tb):
1930        """
1931        Thread threw an exception.
1932        """
1933        # TODO: remimplement thread cancellation
1934        logging.error("".join(traceback.format_exception(etype, value, tb)))
1935
1936    def setTableProperties(self, table):
1937        """
1938        Setting table properties
1939        """
1940        # Table properties
1941        table.verticalHeader().setVisible(False)
1942        table.setAlternatingRowColors(True)
1943        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
1944        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
1945        table.resizeColumnsToContents()
1946
1947        # Header
1948        header = table.horizontalHeader()
1949        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
1950        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
1951
1952        # Qt5: the following 2 lines crash - figure out why!
1953        # Resize column 0 and 7 to content
1954        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
1955        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
1956
1957    def setPolyModel(self):
1958        """
1959        Set polydispersity values
1960        """
1961        if not self.model_parameters:
1962            return
1963        self._poly_model.clear()
1964
1965        [self.setPolyModelParameters(i, param) for i, param in \
1966            enumerate(self.model_parameters.form_volume_parameters) if param.polydisperse]
1967        FittingUtilities.addPolyHeadersToModel(self._poly_model)
1968
1969    def setPolyModelParameters(self, i, param):
1970        """
1971        Standard of multishell poly parameter driver
1972        """
1973        param_name = param.name
1974        # see it the parameter is multishell
1975        if '[' in param.name:
1976            # Skip empty shells
1977            if self.current_shell_displayed == 0:
1978                return
1979            else:
1980                # Create as many entries as current shells
1981                for ishell in range(1, self.current_shell_displayed+1):
1982                    # Remove [n] and add the shell numeral
1983                    name = param_name[0:param_name.index('[')] + str(ishell)
1984                    self.addNameToPolyModel(i, name)
1985        else:
1986            # Just create a simple param entry
1987            self.addNameToPolyModel(i, param_name)
1988
1989    def addNameToPolyModel(self, i, param_name):
1990        """
1991        Creates a checked row in the poly model with param_name
1992        """
1993        # Polydisp. values from the sasmodel
1994        width = self.kernel_module.getParam(param_name + '.width')
1995        npts = self.kernel_module.getParam(param_name + '.npts')
1996        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
1997        _, min, max = self.kernel_module.details[param_name]
1998
1999        # Construct a row with polydisp. related variable.
2000        # This will get added to the polydisp. model
2001        # Note: last argument needs extra space padding for decent display of the control
2002        checked_list = ["Distribution of " + param_name, str(width),
2003                        str(min), str(max),
2004                        str(npts), str(nsigs), "gaussian      ",'']
2005        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2006
2007        # All possible polydisp. functions as strings in combobox
2008        func = QtWidgets.QComboBox()
2009        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
2010        # Set the default index
2011        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
2012        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2013        self.lstPoly.setIndexWidget(ind, func)
2014        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2015
2016    def onPolyFilenameChange(self, row_index):
2017        """
2018        Respond to filename_updated signal from the delegate
2019        """
2020        # For the given row, invoke the "array" combo handler
2021        array_caption = 'array'
2022
2023        # Get the combo box reference
2024        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2025        widget = self.lstPoly.indexWidget(ind)
2026
2027        # Update the combo box so it displays "array"
2028        widget.blockSignals(True)
2029        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
2030        widget.blockSignals(False)
2031
2032        # Invoke the file reader
2033        self.onPolyComboIndexChange(array_caption, row_index)
2034
2035    def onPolyComboIndexChange(self, combo_string, row_index):
2036        """
2037        Modify polydisp. defaults on function choice
2038        """
2039        # Get npts/nsigs for current selection
2040        param = self.model_parameters.form_volume_parameters[row_index]
2041        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2042        combo_box = self.lstPoly.indexWidget(file_index)
2043
2044        def updateFunctionCaption(row):
2045            # Utility function for update of polydispersity function name in the main model
2046            param_name = str(self._model_model.item(row, 0).text())
2047            if param_name !=  param.name:
2048                return
2049            # Modify the param value
2050            self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
2051
2052        if combo_string == 'array':
2053            try:
2054                self.loadPolydispArray(row_index)
2055                # Update main model for display
2056                self.iterateOverModel(updateFunctionCaption)
2057                # disable the row
2058                lo = self.lstPoly.itemDelegate().poly_pd
2059                hi = self.lstPoly.itemDelegate().poly_function
2060                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
2061                return
2062            except IOError:
2063                combo_box.setCurrentIndex(self.orig_poly_index)
2064                # Pass for cancel/bad read
2065                pass
2066
2067        # Enable the row in case it was disabled by Array
2068        self._poly_model.blockSignals(True)
2069        max_range = self.lstPoly.itemDelegate().poly_filename
2070        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
2071        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2072        self._poly_model.setData(file_index, "")
2073        self._poly_model.blockSignals(False)
2074
2075        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
2076        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
2077
2078        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
2079        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
2080
2081        self._poly_model.setData(npts_index, npts)
2082        self._poly_model.setData(nsigs_index, nsigs)
2083
2084        self.iterateOverModel(updateFunctionCaption)
2085        self.orig_poly_index = combo_box.currentIndex()
2086
2087    def loadPolydispArray(self, row_index):
2088        """
2089        Show the load file dialog and loads requested data into state
2090        """
2091        datafile = QtWidgets.QFileDialog.getOpenFileName(
2092            self, "Choose a weight file", "", "All files (*.*)", None,
2093            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
2094
2095        if not datafile:
2096            logging.info("No weight data chosen.")
2097            raise IOError
2098
2099        values = []
2100        weights = []
2101        def appendData(data_tuple):
2102            """
2103            Fish out floats from a tuple of strings
2104            """
2105            try:
2106                values.append(float(data_tuple[0]))
2107                weights.append(float(data_tuple[1]))
2108            except (ValueError, IndexError):
2109                # just pass through if line with bad data
2110                return
2111
2112        with open(datafile, 'r') as column_file:
2113            column_data = [line.rstrip().split() for line in column_file.readlines()]
2114            [appendData(line) for line in column_data]
2115
2116        # If everything went well - update the sasmodel values
2117        self.disp_model = POLYDISPERSITY_MODELS['array']()
2118        self.disp_model.set_weights(np.array(values), np.array(weights))
2119        # + update the cell with filename
2120        fname = os.path.basename(str(datafile))
2121        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
2122        self._poly_model.setData(fname_index, fname)
2123
2124    def setMagneticModel(self):
2125        """
2126        Set magnetism values on model
2127        """
2128        if not self.model_parameters:
2129            return
2130        self._magnet_model.clear()
2131        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
2132            self.model_parameters.call_parameters if param.type == 'magnetic']
2133        FittingUtilities.addHeadersToModel(self._magnet_model)
2134
2135    def shellNamesList(self):
2136        """
2137        Returns list of names of all multi-shell parameters
2138        E.g. for sld[n], radius[n], n=1..3 it will return
2139        [sld1, sld2, sld3, radius1, radius2, radius3]
2140        """
2141        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
2142        top_index = self.kernel_module.multiplicity_info.number
2143        shell_names = []
2144        for i in range(1, top_index+1):
2145            for name in multi_names:
2146                shell_names.append(name+str(i))
2147        return shell_names
2148
2149    def addCheckedMagneticListToModel(self, param, model):
2150        """
2151        Wrapper for model update with a subset of magnetic parameters
2152        """
2153        if param.name[param.name.index(':')+1:] in self.shell_names:
2154            # check if two-digit shell number
2155            try:
2156                shell_index = int(param.name[-2:])
2157            except ValueError:
2158                shell_index = int(param.name[-1:])
2159
2160            if shell_index > self.current_shell_displayed:
2161                return
2162
2163        checked_list = [param.name,
2164                        str(param.default),
2165                        str(param.limits[0]),
2166                        str(param.limits[1]),
2167                        param.units]
2168
2169        FittingUtilities.addCheckedListToModel(model, checked_list)
2170
2171    def enableStructureFactorControl(self, structure_factor):
2172        """
2173        Add structure factors to the list of parameters
2174        """
2175        if self.kernel_module.is_form_factor or structure_factor == 'None':
2176            self.enableStructureCombo()
2177        else:
2178            self.disableStructureCombo()
2179
2180    def addExtraShells(self):
2181        """
2182        Add a combobox for multiple shell display
2183        """
2184        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
2185
2186        if param_length == 0:
2187            return
2188
2189        # cell 1: variable name
2190        item1 = QtGui.QStandardItem(param_name)
2191
2192        func = QtWidgets.QComboBox()
2193        # Available range of shells displayed in the combobox
2194        func.addItems([str(i) for i in range(param_length+1)])
2195
2196        # Respond to index change
2197        func.currentIndexChanged.connect(self.modifyShellsInList)
2198
2199        # cell 2: combobox
2200        item2 = QtGui.QStandardItem()
2201        self._model_model.appendRow([item1, item2])
2202
2203        # Beautify the row:  span columns 2-4
2204        shell_row = self._model_model.rowCount()
2205        shell_index = self._model_model.index(shell_row-1, 1)
2206
2207        self.lstParams.setIndexWidget(shell_index, func)
2208        self._last_model_row = self._model_model.rowCount()
2209
2210        # Set the index to the state-kept value
2211        func.setCurrentIndex(self.current_shell_displayed
2212                             if self.current_shell_displayed < func.count() else 0)
2213
2214    def modifyShellsInList(self, index):
2215        """
2216        Add/remove additional multishell parameters
2217        """
2218        # Find row location of the combobox
2219        last_row = self._last_model_row
2220        remove_rows = self._model_model.rowCount() - last_row
2221
2222        if remove_rows > 1:
2223            self._model_model.removeRows(last_row, remove_rows)
2224
2225        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
2226        self.current_shell_displayed = index
2227
2228        # Update relevant models
2229        self.setPolyModel()
2230        self.setMagneticModel()
2231
2232    def setFittingStarted(self):
2233        """
2234        Set item enablement on fitting start
2235        """
2236        #disable the Fit button
2237        self.cmdFit.setText('Running...')
2238        self.cmdFit.setEnabled(False)
2239
2240    def setFittingStopped(self):
2241        """
2242        Set item enablement on fitting stop
2243        """
2244        #enable the Fit button
2245        self.cmdFit.setText("Fit")
2246        self.cmdFit.setEnabled(True)
2247
2248    def readFitPage(self, fp):
2249        """
2250        Read in state from a fitpage object and update GUI
2251        """
2252        assert isinstance(fp, FitPage)
2253        # Main tab info
2254        self.logic.data.filename = fp.filename
2255        self.data_is_loaded = fp.data_is_loaded
2256        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
2257        self.chkMagnetism.setCheckState(fp.is_magnetic)
2258        self.chk2DView.setCheckState(fp.is2D)
2259
2260        # Update the comboboxes
2261        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
2262        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
2263        if fp.current_factor:
2264            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
2265
2266        self.chi2 = fp.chi2
2267
2268        # Options tab
2269        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
2270        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
2271        self.npts = fp.fit_options[fp.NPTS]
2272        self.log_points = fp.fit_options[fp.LOG_POINTS]
2273        self.weighting = fp.fit_options[fp.WEIGHTING]
2274
2275        # Models
2276        self._model_model = fp.model_model
2277        self._poly_model = fp.poly_model
2278        self._magnet_model = fp.magnetism_model
2279
2280        # Resolution tab
2281        smearing = fp.smearing_options[fp.SMEARING_OPTION]
2282        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
2283        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
2284        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
2285        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
2286
2287        # TODO: add polidyspersity and magnetism
2288
2289    def saveToFitPage(self, fp):
2290        """
2291        Write current state to the given fitpage
2292        """
2293        assert isinstance(fp, FitPage)
2294
2295        # Main tab info
2296        fp.filename = self.logic.data.filename
2297        fp.data_is_loaded = self.data_is_loaded
2298        fp.is_polydisperse = self.chkPolydispersity.isChecked()
2299        fp.is_magnetic = self.chkMagnetism.isChecked()
2300        fp.is2D = self.chk2DView.isChecked()
2301        fp.data = self.data
2302
2303        # Use current models - they contain all the required parameters
2304        fp.model_model = self._model_model
2305        fp.poly_model = self._poly_model
2306        fp.magnetism_model = self._magnet_model
2307
2308        if self.cbCategory.currentIndex() != 0:
2309            fp.current_category = str(self.cbCategory.currentText())
2310            fp.current_model = str(self.cbModel.currentText())
2311
2312        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
2313            fp.current_factor = str(self.cbStructureFactor.currentText())
2314        else:
2315            fp.current_factor = ''
2316
2317        fp.chi2 = self.chi2
2318        fp.parameters_to_fit = self.parameters_to_fit
2319        fp.kernel_module = self.kernel_module
2320
2321        # Algorithm options
2322        # fp.algorithm = self.parent.fit_options.selected_id
2323
2324        # Options tab
2325        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
2326        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
2327        fp.fit_options[fp.NPTS] = self.npts
2328        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
2329        fp.fit_options[fp.LOG_POINTS] = self.log_points
2330        fp.fit_options[fp.WEIGHTING] = self.weighting
2331
2332        # Resolution tab
2333        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
2334        fp.smearing_options[fp.SMEARING_OPTION] = smearing
2335        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
2336        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
2337        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
2338
2339        # TODO: add polidyspersity and magnetism
2340
2341
2342    def updateUndo(self):
2343        """
2344        Create a new state page and add it to the stack
2345        """
2346        if self.undo_supported:
2347            self.pushFitPage(self.currentState())
2348
2349    def currentState(self):
2350        """
2351        Return fit page with current state
2352        """
2353        new_page = FitPage()
2354        self.saveToFitPage(new_page)
2355
2356        return new_page
2357
2358    def pushFitPage(self, new_page):
2359        """
2360        Add a new fit page object with current state
2361        """
2362        self.page_stack.append(new_page)
2363
2364    def popFitPage(self):
2365        """
2366        Remove top fit page from stack
2367        """
2368        if self.page_stack:
2369            self.page_stack.pop()
2370
Note: See TracBrowser for help on using the repository browser.