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

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 9ba91b7 was 9ba91b7, checked in by ibressler, 6 years ago

Merge branch 'ESS_GUI' into ESS_GUI_iss1033

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