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

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

radius_effective handling

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