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

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

C&S fitting now runs and updates fit tabs - SASVIEW-860

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