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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since f84d793 was f84d793, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

reduce initial width of Data Explorer dock

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