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

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

Editable moniker field, updating the model below.
Improved table behaviour

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