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

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

Replaced 'smart' plot generation with explicit plot requests on "Show Plot". SASVIEW-1018

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