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

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

Simple vs. complex constraints behaviour fixed.

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