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

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

Initial commit for constrained & simultaneous fitting functionality SASVIEW-277

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