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

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

More code review related fixes

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