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

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

Added constraints to the fitter

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