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

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

unit tests for constraints: FittingWidget?, FittingPerspective?

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