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

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

Added batch fit constraints.
Cleaned up interactions between constraints in various tabs

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