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

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

Make sure we get meaningful reports for theories.

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