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

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

fix error column not being reset after structure factor is re-selected

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