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

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

Started work on the complex constraint widget SASVIEW-853

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