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

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

Chain fitting for constraint batch fitting

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