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

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

Don't display DEBUG level messages in the log explorer

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