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

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

Updated references to help files

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