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

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

Separate kernel parameters so they can be selectively added during fitting and calculation. SASVIEW-1008, SASVIEW-1013

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