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

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

Update charts on polydisp. parameter change

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