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

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 0d72cac was 0d72cac, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

[CHERRY-PICK FROM a699172f8] in GUI, split params by P(Q), S(Q)

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