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

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

Don't wantonly cast parameter names into lower case. SASVIEW-1016

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