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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since c1bc9de was d1e4689, checked in by Piotr Rozyczko <piotrrozyczko@…>, 6 years ago

Added status bar display for single value constraints SASVIEW-1043
Fixed an issue with theories not showing up properly

  • Property mode set to 100644
File size: 134.4 KB
RevLine 
[60af928]1import json
[cd31251]2import os
[60af928]3from collections import defaultdict
[b3e8629]4
[d4dac80]5import copy
[5236449]6import logging
7import traceback
[cbcdd2c]8from twisted.internet import threads
[1bc27f1]9import numpy as np
[e90988c]10import webbrowser
[5236449]11
[4992ff2]12from PyQt5 import QtCore
13from PyQt5 import QtGui
14from PyQt5 import QtWidgets
[60af928]15
16from sasmodels import generate
17from sasmodels import modelinfo
[5236449]18from sasmodels.sasview_model import load_standard_models
[18d5c94a]19from sasmodels.sasview_model import MultiplicationModel
[358b39d]20from sasmodels.weights import MODELS as POLYDISPERSITY_MODELS
21
[f182f93]22from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
[57be490]23from sas.sascalc.fit.pagestate import PageState
[5236449]24
[83eb5208]25import sas.qtgui.Utilities.GuiUtils as GuiUtils
[14ec91c5]26import sas.qtgui.Utilities.LocalConfig as LocalConfig
[dc5ef15]27from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
28from sas.qtgui.Plotting.PlotterData import Data1D
29from sas.qtgui.Plotting.PlotterData import Data2D
[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
[d1e4689]1074            constr = self.getConstraintForRow(row)
[c4b23dd]1075            func = self.getConstraintForRow(row).func
[d1e4689]1076            if constr.func is not None:
1077                # inter-parameter constraint
1078                update_text = "Active constraint: "+func
1079            elif constr.param == rows[0].data():
1080                # current value constraint
1081                update_text = "Value constrained to: " + str(constr.value)
1082            else:
1083                # ill defined constraint
1084                return
1085            self.communicate.statusBarUpdateSignal.emit(update_text)
[3b3b40b]1086
[47d7d2d]1087    def replaceConstraintName(self, old_name, new_name=""):
1088        """
1089        Replace names of models in defined constraints
1090        """
1091        param_number = self._model_model.rowCount()
1092        # loop over parameters
1093        for row in range(param_number):
1094            if self.rowHasConstraint(row):
1095                func = self._model_model.item(row, 1).child(0).data().func
1096                if old_name in func:
1097                    new_func = func.replace(old_name, new_name)
1098                    self._model_model.item(row, 1).child(0).data().func = new_func
1099
[3b3b40b]1100    def isConstraintMultimodel(self, constraint):
1101        """
1102        Check if the constraint function text contains current model name
1103        """
1104        current_model_name = self.kernel_module.name
1105        if current_model_name in constraint:
1106            return False
1107        else:
1108            return True
1109
[9c0ce68]1110    def updateData(self):
1111        """
1112        Helper function for recalculation of data used in plotting
1113        """
1114        # Update the chart
[0268aed]1115        if self.data_is_loaded:
[d48cc19]1116            self.cmdPlot.setText("Show Plot")
[0268aed]1117            self.calculateQGridForModel()
1118        else:
[d48cc19]1119            self.cmdPlot.setText("Calculate")
[0268aed]1120            # Create default datasets if no data passed
1121            self.createDefaultDataset()
1122
[9c0ce68]1123    def respondToModelStructure(self, model=None, structure_factor=None):
1124        # Set enablement on calculate/plot
1125        self.cmdPlot.setEnabled(True)
1126
1127        # kernel parameters -> model_model
1128        self.SASModelToQModel(model, structure_factor)
1129
[f37cab0]1130        for column, width in self.lstParamHeaderSizes.items():
1131            self.lstParams.setColumnWidth(column, width)
1132
[9c0ce68]1133        # Update plot
1134        self.updateData()
1135
[6011788]1136        # Update state stack
[00b3b40]1137        self.updateUndo()
[2add354]1138
[be8f4b0]1139        # Let others know
1140        self.newModelSignal.emit()
1141
[cd31251]1142    def onSelectCategory(self):
[60af928]1143        """
1144        Select Category from list
1145        """
[d6b8a1d]1146        category = self.cbCategory.currentText()
[86f88d1]1147        # Check if the user chose "Choose category entry"
[4d457df]1148        if category == CATEGORY_DEFAULT:
[86f88d1]1149            # if the previous category was not the default, keep it.
1150            # Otherwise, just return
1151            if self._previous_category_index != 0:
[351b53e]1152                # We need to block signals, or else state changes on perceived unchanged conditions
1153                self.cbCategory.blockSignals(True)
[86f88d1]1154                self.cbCategory.setCurrentIndex(self._previous_category_index)
[351b53e]1155                self.cbCategory.blockSignals(False)
[86f88d1]1156            return
1157
[4d457df]1158        if category == CATEGORY_STRUCTURE:
[6f7f652]1159            self.disableModelCombo()
1160            self.enableStructureCombo()
[f4aa7a8]1161            # set the index to 0
1162            self.cbStructureFactor.setCurrentIndex(0)
1163            self.model_parameters = None
[29eb947]1164            self._model_model.clear()
[6f7f652]1165            return
[8b480d27]1166
[cbcdd2c]1167        # Safely clear and enable the model combo
[6f7f652]1168        self.cbModel.blockSignals(True)
1169        self.cbModel.clear()
1170        self.cbModel.blockSignals(False)
1171        self.enableModelCombo()
1172        self.disableStructureCombo()
1173
[86f88d1]1174        self._previous_category_index = self.cbCategory.currentIndex()
[cbcdd2c]1175        # Retrieve the list of models
[4d457df]1176        model_list = self.master_category_dict[category]
[cbcdd2c]1177        # Populate the models combobox
[b1e36a3]1178        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
[4d457df]1179
[0268aed]1180    def onPolyModelChange(self, item):
1181        """
1182        Callback method for updating the main model and sasmodel
1183        parameters with the GUI values in the polydispersity view
1184        """
1185        model_column = item.column()
1186        model_row = item.row()
1187        name_index = self._poly_model.index(model_row, 0)
[287d356]1188        parameter_name = str(name_index.data()) # "distribution of sld" etc.
1189        if "istribution of" in parameter_name:
[358b39d]1190            # just the last word
1191            parameter_name = parameter_name.rsplit()[-1]
[c1e380e]1192
[906e0c7]1193        delegate = self.lstPoly.itemDelegate()
1194
[06b0138]1195        # Extract changed value.
[906e0c7]1196        if model_column == delegate.poly_parameter:
[00b3b40]1197            # Is the parameter checked for fitting?
[0268aed]1198            value = item.checkState()
[1643d8ed]1199            parameter_name = parameter_name + '.width'
[c1e380e]1200            if value == QtCore.Qt.Checked:
[6dbff18]1201                self.poly_params_to_fit.append(parameter_name)
[c1e380e]1202            else:
[6dbff18]1203                if parameter_name in self.poly_params_to_fit:
1204                    self.poly_params_to_fit.remove(parameter_name)
1205            self.cmdFit.setEnabled(self.haveParamsToFit())
[906e0c7]1206
1207        elif model_column in [delegate.poly_min, delegate.poly_max]:
[aca8418]1208            try:
[fbfc488]1209                value = GuiUtils.toDouble(item.text())
[0261bc1]1210            except TypeError:
[aca8418]1211                # Can't be converted properly, bring back the old value and exit
1212                return
1213
1214            current_details = self.kernel_module.details[parameter_name]
[906e0c7]1215            if self.has_poly_error_column:
1216                # err column changes the indexing
1217                current_details[model_column-2] = value
1218            else:
1219                current_details[model_column-1] = value
1220
1221        elif model_column == delegate.poly_function:
[919d47c]1222            # name of the function - just pass
[906e0c7]1223            pass
1224
[0268aed]1225        else:
1226            try:
[fbfc488]1227                value = GuiUtils.toDouble(item.text())
[0261bc1]1228            except TypeError:
[0268aed]1229                # Can't be converted properly, bring back the old value and exit
1230                return
1231
[aca8418]1232            # Update the sasmodel
1233            # PD[ratio] -> width, npts -> npts, nsigs -> nsigmas
[66d4370]1234            #self.kernel_module.setParam(parameter_name + '.' + delegate.columnDict()[model_column], value)
1235            key = parameter_name + '.' + delegate.columnDict()[model_column]
1236            self.poly_params[key] = value
[0268aed]1237
[9c0ce68]1238            # Update plot
1239            self.updateData()
1240
[906e0c7]1241        # update in param model
1242        if model_column in [delegate.poly_pd, delegate.poly_error, delegate.poly_min, delegate.poly_max]:
1243            row = self.getRowFromName(parameter_name)
[f3cc979]1244            param_item = self._model_model.item(row).child(0).child(0, model_column)
1245            if param_item is None:
1246                return
[73665a8]1247            self._model_model.blockSignals(True)
[f3cc979]1248            param_item.setText(item.text())
[73665a8]1249            self._model_model.blockSignals(False)
[906e0c7]1250
[b00414d]1251    def onMagnetModelChange(self, item):
1252        """
1253        Callback method for updating the sasmodel magnetic parameters with the GUI values
1254        """
1255        model_column = item.column()
1256        model_row = item.row()
1257        name_index = self._magnet_model.index(model_row, 0)
[fbfc488]1258        parameter_name = str(self._magnet_model.data(name_index))
[b00414d]1259
1260        if model_column == 0:
1261            value = item.checkState()
1262            if value == QtCore.Qt.Checked:
[6dbff18]1263                self.magnet_params_to_fit.append(parameter_name)
[b00414d]1264            else:
[6dbff18]1265                if parameter_name in self.magnet_params_to_fit:
1266                    self.magnet_params_to_fit.remove(parameter_name)
1267            self.cmdFit.setEnabled(self.haveParamsToFit())
[b00414d]1268            # Update state stack
1269            self.updateUndo()
1270            return
1271
1272        # Extract changed value.
1273        try:
[fbfc488]1274            value = GuiUtils.toDouble(item.text())
[0261bc1]1275        except TypeError:
[b00414d]1276            # Unparsable field
1277            return
[66d4370]1278        delegate = self.lstMagnetic.itemDelegate()
1279
1280        if model_column > 1:
1281            if model_column == delegate.mag_min:
1282                pos = 1
1283            elif model_column == delegate.mag_max:
1284                pos = 2
1285            elif model_column == delegate.mag_unit:
1286                pos = 0
1287            else:
1288                raise AttributeError("Wrong column in magnetism table.")
1289            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1290            self.kernel_module.details[parameter_name][pos] = value
1291        else:
1292            self.magnet_params[parameter_name] = value
1293            #self.kernel_module.setParam(parameter_name) = value
1294            # Force the chart update when actual parameters changed
[b00414d]1295            self.recalculatePlotData()
1296
1297        # Update state stack
1298        self.updateUndo()
1299
[2add354]1300    def onHelp(self):
1301        """
1302        Show the "Fitting" section of help
1303        """
[aed0532]1304        tree_location = "/user/qtgui/Perspectives/Fitting/"
[70080a0]1305
1306        # Actual file will depend on the current tab
1307        tab_id = self.tabFitting.currentIndex()
1308        helpfile = "fitting.html"
1309        if tab_id == 0:
1310            helpfile = "fitting_help.html"
1311        elif tab_id == 1:
1312            helpfile = "residuals_help.html"
1313        elif tab_id == 2:
[e90988c]1314            helpfile = "resolution.html"
[70080a0]1315        elif tab_id == 3:
[e90988c]1316            helpfile = "pd/polydispersity.html"
[70080a0]1317        elif tab_id == 4:
[e90988c]1318            helpfile = "magnetism/magnetism.html"
[70080a0]1319        help_location = tree_location + helpfile
[d6b8a1d]1320
[e90988c]1321        self.showHelp(help_location)
1322
1323    def showHelp(self, url):
1324        """
1325        Calls parent's method for opening an HTML page
1326        """
1327        self.parent.showHelp(url)
[2add354]1328
[6ff2eb3]1329    def onDisplayMagneticAngles(self):
1330        """
1331        Display a simple image showing direction of magnetic angles
1332        """
1333        self.magneticAnglesWidget.show()
1334
[0268aed]1335    def onFit(self):
1336        """
1337        Perform fitting on the current data
1338        """
[ded5e77]1339        if self.fit_started:
1340            self.stopFit()
1341            return
1342
[116dd4c1]1343        # initialize fitter constants
[f182f93]1344        fit_id = 0
1345        handler = None
1346        batch_inputs = {}
1347        batch_outputs = {}
1348        #---------------------------------
[14ec91c5]1349        if LocalConfig.USING_TWISTED:
[7adc2a8]1350            handler = None
1351            updater = None
1352        else:
1353            handler = ConsoleUpdate(parent=self.parent,
1354                                    manager=self,
1355                                    improvement_delta=0.1)
1356            updater = handler.update_fit
[f182f93]1357
[116dd4c1]1358        # Prepare the fitter object
[c6343a5]1359        try:
1360            fitters, _ = self.prepareFitters()
1361        except ValueError as ex:
1362            # This should not happen! GUI explicitly forbids this situation
[3b3b40b]1363            self.communicate.statusBarUpdateSignal.emit(str(ex))
[c6343a5]1364            return
[f182f93]1365
[d4dac80]1366        # keep local copy of kernel parameters, as they will change during the update
1367        self.kernel_module_copy = copy.deepcopy(self.kernel_module)
1368
[f182f93]1369        # Create the fitting thread, based on the fitter
[3b3b40b]1370        completefn = self.batchFittingCompleted if self.is_batch_fitting else self.fittingCompleted
[ee18d33]1371
[ded5e77]1372        self.calc_fit = FitThread(handler=handler,
[116dd4c1]1373                            fn=fitters,
1374                            batch_inputs=batch_inputs,
1375                            batch_outputs=batch_outputs,
1376                            page_id=[[self.page_id]],
1377                            updatefn=updater,
[91ad45c]1378                            completefn=completefn,
1379                            reset_flag=self.is_chain_fitting)
[7adc2a8]1380
[14ec91c5]1381        if LocalConfig.USING_TWISTED:
[7adc2a8]1382            # start the trhrhread with twisted
[ded5e77]1383            calc_thread = threads.deferToThread(self.calc_fit.compute)
[14ec91c5]1384            calc_thread.addCallback(completefn)
[7adc2a8]1385            calc_thread.addErrback(self.fitFailed)
1386        else:
1387            # Use the old python threads + Queue
[ded5e77]1388            self.calc_fit.queue()
1389            self.calc_fit.ready(2.5)
[f182f93]1390
[d7ff531]1391        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
[ded5e77]1392        self.fit_started = True
[557fc498]1393
[14ec91c5]1394        # Disable some elements
[557fc498]1395        self.disableInteractiveElements()
[0268aed]1396
[ded5e77]1397    def stopFit(self):
1398        """
1399        Attempt to stop the fitting thread
1400        """
1401        if self.calc_fit is None or not self.calc_fit.isrunning():
1402            return
1403        self.calc_fit.stop()
1404        #re-enable the Fit button
[557fc498]1405        self.enableInteractiveElements()
[ded5e77]1406
1407        msg = "Fitting cancelled."
1408        self.communicate.statusBarUpdateSignal.emit(msg)
1409
[f182f93]1410    def updateFit(self):
1411        """
1412        """
[b3e8629]1413        print("UPDATE FIT")
[0268aed]1414        pass
1415
[02ddfb4]1416    def fitFailed(self, reason):
1417        """
1418        """
[557fc498]1419        self.enableInteractiveElements()
[ded5e77]1420        msg = "Fitting failed with: "+ str(reason)
1421        self.communicate.statusBarUpdateSignal.emit(msg)
[02ddfb4]1422
[3b3b40b]1423    def batchFittingCompleted(self, result):
1424        """
1425        Send the finish message from calculate threads to main thread
1426        """
[a2cc8b97]1427        if result is None:
1428            result = tuple()
[3b3b40b]1429        self.batchFittingFinishedSignal.emit(result)
1430
[ee18d33]1431    def batchFitComplete(self, result):
1432        """
1433        Receive and display batch fitting results
1434        """
1435        #re-enable the Fit button
[557fc498]1436        self.enableInteractiveElements()
[d4dac80]1437
[a2cc8b97]1438        if len(result) == 0:
[d4dac80]1439            msg = "Fitting failed."
1440            self.communicate.statusBarUpdateSignal.emit(msg)
1441            return
1442
[3b3b40b]1443        # Show the grid panel
[d4dac80]1444        self.communicate.sendDataToGridSignal.emit(result[0])
1445
1446        elapsed = result[1]
1447        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
1448        self.communicate.statusBarUpdateSignal.emit(msg)
1449
1450        # Run over the list of results and update the items
1451        for res_index, res_list in enumerate(result[0]):
1452            # results
1453            res = res_list[0]
1454            param_dict = self.paramDictFromResults(res)
1455
1456            # create local kernel_module
1457            kernel_module = FittingUtilities.updateKernelWithResults(self.kernel_module, param_dict)
1458            # pull out current data
1459            data = self._logic[res_index].data
1460
1461            # Switch indexes
1462            self.onSelectBatchFilename(res_index)
1463
1464            method = self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1465            self.calculateQGridForModelExt(data=data, model=kernel_module, completefn=method, use_threads=False)
1466
1467        # Restore original kernel_module, so subsequent fits on the same model don't pick up the new params
1468        if self.kernel_module is not None:
1469            self.kernel_module = copy.deepcopy(self.kernel_module_copy)
1470
1471    def paramDictFromResults(self, results):
1472        """
1473        Given the fit results structure, pull out optimized parameters and return them as nicely
1474        formatted dict
1475        """
1476        if results.fitness is None or \
1477            not np.isfinite(results.fitness) or \
1478            np.any(results.pvec is None) or \
1479            not np.all(np.isfinite(results.pvec)):
1480            msg = "Fitting did not converge!"
1481            self.communicate.statusBarUpdateSignal.emit(msg)
1482            msg += results.mesg
[bb477f5]1483            logger.error(msg)
[d4dac80]1484            return
1485
1486        param_list = results.param_list # ['radius', 'radius.width']
1487        param_values = results.pvec     # array([ 0.36221662,  0.0146783 ])
1488        param_stderr = results.stderr   # array([ 1.71293015,  1.71294233])
1489        params_and_errors = list(zip(param_values, param_stderr))
1490        param_dict = dict(zip(param_list, params_and_errors))
1491
1492        return param_dict
[3b3b40b]1493
1494    def fittingCompleted(self, result):
1495        """
1496        Send the finish message from calculate threads to main thread
1497        """
[a2cc8b97]1498        if result is None:
1499            result = tuple()
[3b3b40b]1500        self.fittingFinishedSignal.emit(result)
[ee18d33]1501
[f182f93]1502    def fitComplete(self, result):
1503        """
1504        Receive and display fitting results
1505        "result" is a tuple of actual result list and the fit time in seconds
1506        """
1507        #re-enable the Fit button
[557fc498]1508        self.enableInteractiveElements()
[d7ff531]1509
[a2cc8b97]1510        if len(result) == 0:
[3b3b40b]1511            msg = "Fitting failed."
[06234fc]1512            self.communicate.statusBarUpdateSignal.emit(msg)
1513            return
[d7ff531]1514
[ee18d33]1515        res_list = result[0][0]
[f182f93]1516        res = res_list[0]
[d4dac80]1517        self.chi2 = res.fitness
1518        param_dict = self.paramDictFromResults(res)
[f182f93]1519
[14acf92]1520        if param_dict is None:
1521            return
1522
[f182f93]1523        elapsed = result[1]
[ded5e77]1524        if self.calc_fit._interrupting:
1525            msg = "Fitting cancelled by user after: %s s." % GuiUtils.formatNumber(elapsed)
[bb477f5]1526            logger.warning("\n"+msg+"\n")
[ded5e77]1527        else:
1528            msg = "Fitting completed successfully in: %s s." % GuiUtils.formatNumber(elapsed)
[f182f93]1529        self.communicate.statusBarUpdateSignal.emit(msg)
1530
1531        # Dictionary of fitted parameter: value, error
1532        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
1533        self.updateModelFromList(param_dict)
1534
[aca8418]1535        self.updatePolyModelFromList(param_dict)
1536
[b00414d]1537        self.updateMagnetModelFromList(param_dict)
1538
[d7ff531]1539        # update charts
1540        self.onPlot()
[c2f3ca2]1541        #self.recalculatePlotData()
1542
[d7ff531]1543
[f182f93]1544        # Read only value - we can get away by just printing it here
[2add354]1545        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
[f182f93]1546        self.lblChi2Value.setText(chi2_repr)
1547
[116dd4c1]1548    def prepareFitters(self, fitter=None, fit_id=0):
1549        """
1550        Prepare the Fitter object for use in fitting
1551        """
1552        # fitter = None -> single/batch fitting
1553        # fitter = Fit() -> simultaneous fitting
1554
1555        # Data going in
1556        data = self.logic.data
[66d4370]1557        model = copy.deepcopy(self.kernel_module)
[116dd4c1]1558        qmin = self.q_range_min
1559        qmax = self.q_range_max
[66d4370]1560        # add polydisperse/magnet parameters if asked
1561        self.updateKernelModelWithExtraParams(model)
[6dbff18]1562
1563        params_to_fit = self.main_params_to_fit
1564        if self.chkPolydispersity.isChecked():
1565            params_to_fit += self.poly_params_to_fit
1566        if self.chkMagnetism.isChecked():
1567            params_to_fit += self.magnet_params_to_fit
[8e2cd79]1568        if not params_to_fit:
[c6343a5]1569            raise ValueError('Fitting requires at least one parameter to optimize.')
[116dd4c1]1570
[8b480d27]1571        # Get the constraints.
1572        constraints = self.getComplexConstraintsForModel()
1573        if fitter is None:
1574            # For single fits - check for inter-model constraints
1575            constraints = self.getConstraintsForFitting()
[3b3b40b]1576
[9a7c81c]1577        smearer = self.smearing_widget.smearer()
[116dd4c1]1578        handler = None
1579        batch_inputs = {}
1580        batch_outputs = {}
1581
1582        fitters = []
1583        for fit_index in self.all_data:
1584            fitter_single = Fit() if fitter is None else fitter
1585            data = GuiUtils.dataFromItem(fit_index)
[9a7c81c]1586            # Potential weights added directly to data
[b764ae5]1587            weighted_data = self.addWeightingToData(data)
[116dd4c1]1588            try:
[b764ae5]1589                fitter_single.set_model(model, fit_id, params_to_fit, data=weighted_data,
[116dd4c1]1590                             constraints=constraints)
1591            except ValueError as ex:
[3b3b40b]1592                raise ValueError("Setting model parameters failed with: %s" % ex)
[116dd4c1]1593
[b764ae5]1594            qmin, qmax, _ = self.logic.computeRangeFromData(weighted_data)
1595            fitter_single.set_data(data=weighted_data, id=fit_id, smearer=smearer, qmin=qmin,
[116dd4c1]1596                            qmax=qmax)
1597            fitter_single.select_problem_for_fit(id=fit_id, value=1)
1598            if fitter is None:
1599                # Assign id to the new fitter only
1600                fitter_single.fitter_id = [self.page_id]
1601            fit_id += 1
1602            fitters.append(fitter_single)
1603
1604        return fitters, fit_id
1605
[f182f93]1606    def iterateOverModel(self, func):
1607        """
1608        Take func and throw it inside the model row loop
1609        """
[b3e8629]1610        for row_i in range(self._model_model.rowCount()):
[f182f93]1611            func(row_i)
1612
1613    def updateModelFromList(self, param_dict):
1614        """
1615        Update the model with new parameters, create the errors column
1616        """
1617        assert isinstance(param_dict, dict)
1618        if not dict:
1619            return
1620
[919d47c]1621        def updateFittedValues(row):
[f182f93]1622            # Utility function for main model update
[d7ff531]1623            # internal so can use closure for param_dict
[919d47c]1624            param_name = str(self._model_model.item(row, 0).text())
[00b7ddf0]1625            if not self.isCheckable(row) or param_name not in list(param_dict.keys()):
[f182f93]1626                return
1627            # modify the param value
[454670d]1628            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
[919d47c]1629            self._model_model.item(row, 1).setText(param_repr)
[c2f3ca2]1630            self.kernel_module.setParam(param_name, param_dict[param_name][0])
[f182f93]1631            if self.has_error_column:
[454670d]1632                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
[919d47c]1633                self._model_model.item(row, 2).setText(error_repr)
[f182f93]1634
[919d47c]1635        def updatePolyValues(row):
1636            # Utility function for updateof polydispersity part of the main model
1637            param_name = str(self._model_model.item(row, 0).text())+'.width'
[00b7ddf0]1638            if not self.isCheckable(row) or param_name not in list(param_dict.keys()):
[919d47c]1639                return
1640            # modify the param value
1641            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1642            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
[906e0c7]1643            # modify the param error
1644            if self.has_error_column:
1645                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1646                self._model_model.item(row, 0).child(0).child(0,2).setText(error_repr)
[919d47c]1647
1648        def createErrorColumn(row):
[f182f93]1649            # Utility function for error column update
1650            item = QtGui.QStandardItem()
[919d47c]1651            def createItem(param_name):
[f182f93]1652                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1653                item.setText(error_repr)
[919d47c]1654            def curr_param():
1655                return str(self._model_model.item(row, 0).text())
1656
[b3e8629]1657            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
[919d47c]1658
[f182f93]1659            error_column.append(item)
1660
[906e0c7]1661        def createPolyErrorColumn(row):
1662            # Utility function for error column update in the polydispersity sub-rows
1663            # NOTE: only creates empty items; updatePolyValues adds the error value
1664            item = self._model_model.item(row, 0)
1665            if not item.hasChildren():
1666                return
1667            poly_item = item.child(0)
1668            if not poly_item.hasChildren():
1669                return
1670            poly_item.insertColumn(2, [QtGui.QStandardItem("")])
1671
1672        if not self.has_error_column:
1673            # create top-level error column
1674            error_column = []
1675            self.lstParams.itemDelegate().addErrorColumn()
1676            self.iterateOverModel(createErrorColumn)
1677
1678            self._model_model.insertColumn(2, error_column)
1679
1680            FittingUtilities.addErrorHeadersToModel(self._model_model)
1681
1682            # create error column in polydispersity sub-rows
1683            self.iterateOverModel(createPolyErrorColumn)
1684
1685            self.has_error_column = True
1686
[c2f3ca2]1687        # block signals temporarily, so we don't end up
1688        # updating charts with every single model change on the end of fitting
1689        self._model_model.itemChanged.disconnect()
[d7ff531]1690        self.iterateOverModel(updateFittedValues)
[919d47c]1691        self.iterateOverModel(updatePolyValues)
[c2f3ca2]1692        self._model_model.itemChanged.connect(self.onMainParamsChange)
[f182f93]1693
[8e2cd79]1694    def iterateOverPolyModel(self, func):
1695        """
1696        Take func and throw it inside the poly model row loop
1697        """
1698        for row_i in range(self._poly_model.rowCount()):
1699            func(row_i)
1700
[aca8418]1701    def updatePolyModelFromList(self, param_dict):
1702        """
1703        Update the polydispersity model with new parameters, create the errors column
1704        """
1705        assert isinstance(param_dict, dict)
1706        if not dict:
1707            return
1708
1709        def updateFittedValues(row_i):
1710            # Utility function for main model update
1711            # internal so can use closure for param_dict
1712            if row_i >= self._poly_model.rowCount():
1713                return
1714            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
[b3e8629]1715            if param_name not in list(param_dict.keys()):
[aca8418]1716                return
1717            # modify the param value
1718            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1719            self._poly_model.item(row_i, 1).setText(param_repr)
[c2f3ca2]1720            self.kernel_module.setParam(param_name, param_dict[param_name][0])
[aca8418]1721            if self.has_poly_error_column:
1722                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1723                self._poly_model.item(row_i, 2).setText(error_repr)
1724
1725        def createErrorColumn(row_i):
1726            # Utility function for error column update
1727            if row_i >= self._poly_model.rowCount():
1728                return
1729            item = QtGui.QStandardItem()
[919d47c]1730
1731            def createItem(param_name):
[aca8418]1732                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1733                item.setText(error_repr)
[919d47c]1734
1735            def poly_param():
1736                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
1737
[b3e8629]1738            [createItem(param_name) for param_name in list(param_dict.keys()) if poly_param() == param_name]
[919d47c]1739
[aca8418]1740            error_column.append(item)
1741
1742        # block signals temporarily, so we don't end up
1743        # updating charts with every single model change on the end of fitting
[c2f3ca2]1744        self._poly_model.itemChanged.disconnect()
[8e2cd79]1745        self.iterateOverPolyModel(updateFittedValues)
[c2f3ca2]1746        self._poly_model.itemChanged.connect(self.onPolyModelChange)
[aca8418]1747
1748        if self.has_poly_error_column:
1749            return
1750
[8eaa101]1751        self.lstPoly.itemDelegate().addErrorColumn()
[aca8418]1752        error_column = []
[8e2cd79]1753        self.iterateOverPolyModel(createErrorColumn)
[aca8418]1754
1755        # switch off reponse to model change
1756        self._poly_model.insertColumn(2, error_column)
1757        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
1758
1759        self.has_poly_error_column = True
1760
[8e2cd79]1761    def iterateOverMagnetModel(self, func):
1762        """
1763        Take func and throw it inside the magnet model row loop
1764        """
[30339cc]1765        for row_i in range(self._magnet_model.rowCount()):
[8e2cd79]1766            func(row_i)
1767
[b00414d]1768    def updateMagnetModelFromList(self, param_dict):
1769        """
1770        Update the magnetic model with new parameters, create the errors column
1771        """
1772        assert isinstance(param_dict, dict)
1773        if not dict:
1774            return
[3b3b40b]1775        if self._magnet_model.rowCount() == 0:
[cee5c78]1776            return
[b00414d]1777
1778        def updateFittedValues(row):
1779            # Utility function for main model update
1780            # internal so can use closure for param_dict
[cee5c78]1781            if self._magnet_model.item(row, 0) is None:
1782                return
[b00414d]1783            param_name = str(self._magnet_model.item(row, 0).text())
[b3e8629]1784            if param_name not in list(param_dict.keys()):
[b00414d]1785                return
1786            # modify the param value
1787            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
1788            self._magnet_model.item(row, 1).setText(param_repr)
[c2f3ca2]1789            self.kernel_module.setParam(param_name, param_dict[param_name][0])
[b00414d]1790            if self.has_magnet_error_column:
1791                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1792                self._magnet_model.item(row, 2).setText(error_repr)
1793
1794        def createErrorColumn(row):
1795            # Utility function for error column update
1796            item = QtGui.QStandardItem()
1797            def createItem(param_name):
1798                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
1799                item.setText(error_repr)
1800            def curr_param():
1801                return str(self._magnet_model.item(row, 0).text())
1802
[b3e8629]1803            [createItem(param_name) for param_name in list(param_dict.keys()) if curr_param() == param_name]
[b00414d]1804
1805            error_column.append(item)
1806
1807        # block signals temporarily, so we don't end up
1808        # updating charts with every single model change on the end of fitting
[c2f3ca2]1809        self._magnet_model.itemChanged.disconnect()
[8e2cd79]1810        self.iterateOverMagnetModel(updateFittedValues)
[c2f3ca2]1811        self._magnet_model.itemChanged.connect(self.onMagnetModelChange)
[b00414d]1812
1813        if self.has_magnet_error_column:
1814            return
1815
1816        self.lstMagnetic.itemDelegate().addErrorColumn()
1817        error_column = []
[8e2cd79]1818        self.iterateOverMagnetModel(createErrorColumn)
[b00414d]1819
1820        # switch off reponse to model change
1821        self._magnet_model.insertColumn(2, error_column)
1822        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1823
1824        self.has_magnet_error_column = True
1825
[0268aed]1826    def onPlot(self):
1827        """
1828        Plot the current set of data
1829        """
[d48cc19]1830        # Regardless of previous state, this should now be `plot show` functionality only
1831        self.cmdPlot.setText("Show Plot")
[88e1f57]1832        # Force data recalculation so existing charts are updated
[d1e4689]1833        if not self.data_is_loaded:
1834            self.showTheoryPlot()
1835        else:
1836            self.showPlot()
[0cd98a1]1837        # This is an important processEvent.
1838        # This allows charts to be properly updated in order
1839        # of plots being applied.
1840        QtWidgets.QApplication.processEvents()
[c2f3ca2]1841        self.recalculatePlotData()
[d48cc19]1842
[9a7c81c]1843    def onSmearingOptionsUpdate(self):
1844        """
1845        React to changes in the smearing widget
1846        """
1847        self.calculateQGridForModel()
1848
[d48cc19]1849    def recalculatePlotData(self):
1850        """
1851        Generate a new dataset for model
1852        """
[180bd54]1853        if not self.data_is_loaded:
[0268aed]1854            self.createDefaultDataset()
1855        self.calculateQGridForModel()
1856
[d1e4689]1857    def showTheoryPlot(self):
1858        """
1859        Show the current theory plot in MPL
1860        """
1861        # Show the chart if ready
1862        data_to_show = self.model_data
1863        if self.theory_item is None:
1864            self.recalculatePlotData()
1865        else:
1866            self.communicate.plotRequestedSignal.emit([self.theory_item, data_to_show], self.tab_id)
1867
[d48cc19]1868    def showPlot(self):
1869        """
1870        Show the current plot in MPL
1871        """
1872        # Show the chart if ready
[d1e4689]1873        data_to_show = self.data
[9ce69ec]1874        # Any models for this page
1875        current_index = self.all_data[self.data_index]
1876        fitpage_name = "" if id is None else "M"+str(self.tab_id)
[d1e4689]1877        plots = GuiUtils.plotsFromFilename(self.data.filename, current_index.model())
[9ce69ec]1878        # Has the fitted data been shown?
1879        data_shown = False
1880        #for plot in plots:
1881        for item, plot in plots.items():
1882            if fitpage_name in plot.name:
1883                data_shown = True
1884                self.communicate.plotRequestedSignal.emit([item, plot], self.tab_id)
1885        if not data_shown:
1886            # fit+data has not been shown - show just data
1887            self.communicate.plotRequestedSignal.emit([item, data_to_show], self.tab_id)
[d48cc19]1888
[180bd54]1889    def onOptionsUpdate(self):
[0268aed]1890        """
[180bd54]1891        Update local option values and replot
[0268aed]1892        """
[180bd54]1893        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1894            self.options_widget.state()
[61a92d4]1895        # set Q range labels on the main tab
1896        self.lblMinRangeDef.setText(str(self.q_range_min))
1897        self.lblMaxRangeDef.setText(str(self.q_range_max))
[d48cc19]1898        self.recalculatePlotData()
[6c8fb2c]1899
[0268aed]1900    def setDefaultStructureCombo(self):
1901        """
1902        Fill in the structure factors combo box with defaults
1903        """
1904        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1905        factors = [factor[0] for factor in structure_factor_list]
1906        factors.insert(0, STRUCTURE_DEFAULT)
1907        self.cbStructureFactor.clear()
1908        self.cbStructureFactor.addItems(sorted(factors))
1909
[4d457df]1910    def createDefaultDataset(self):
1911        """
1912        Generate default Dataset 1D/2D for the given model
1913        """
1914        # Create default datasets if no data passed
1915        if self.is2D:
[180bd54]1916            qmax = self.q_range_max/np.sqrt(2)
[4d457df]1917            qstep = self.npts
1918            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
[180bd54]1919            return
1920        elif self.log_points:
1921            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
[1bc27f1]1922            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
[180bd54]1923            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
[4d457df]1924        else:
[180bd54]1925            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
[1bc27f1]1926                                   num=self.npts, endpoint=True)
[180bd54]1927        self.logic.createDefault1dData(interval, self.tab_id)
[60af928]1928
[5236449]1929    def readCategoryInfo(self):
[60af928]1930        """
1931        Reads the categories in from file
1932        """
1933        self.master_category_dict = defaultdict(list)
1934        self.by_model_dict = defaultdict(list)
1935        self.model_enabled_dict = defaultdict(bool)
1936
[cbcdd2c]1937        categorization_file = CategoryInstaller.get_user_file()
1938        if not os.path.isfile(categorization_file):
1939            categorization_file = CategoryInstaller.get_default_file()
1940        with open(categorization_file, 'rb') as cat_file:
[60af928]1941            self.master_category_dict = json.load(cat_file)
[5236449]1942            self.regenerateModelDict()
[60af928]1943
[5236449]1944        # Load the model dict
1945        models = load_standard_models()
1946        for model in models:
1947            self.models[model.name] = model
1948
[3b3b40b]1949        self.readCustomCategoryInfo()
1950
1951    def readCustomCategoryInfo(self):
1952        """
1953        Reads the custom model category
1954        """
1955        #Looking for plugins
1956        self.plugins = list(self.custom_models.values())
1957        plugin_list = []
1958        for name, plug in self.custom_models.items():
1959            self.models[name] = plug
1960            plugin_list.append([name, True])
1961        self.master_category_dict[CATEGORY_CUSTOM] = plugin_list
1962
[5236449]1963    def regenerateModelDict(self):
[60af928]1964        """
[cbcdd2c]1965        Regenerates self.by_model_dict which has each model name as the
[60af928]1966        key and the list of categories belonging to that model
1967        along with the enabled mapping
1968        """
1969        self.by_model_dict = defaultdict(list)
1970        for category in self.master_category_dict:
1971            for (model, enabled) in self.master_category_dict[category]:
1972                self.by_model_dict[model].append(category)
1973                self.model_enabled_dict[model] = enabled
1974
[86f88d1]1975    def addBackgroundToModel(self, model):
1976        """
1977        Adds background parameter with default values to the model
1978        """
[cbcdd2c]1979        assert isinstance(model, QtGui.QStandardItemModel)
[86f88d1]1980        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
[4d457df]1981        FittingUtilities.addCheckedListToModel(model, checked_list)
[2add354]1982        last_row = model.rowCount()-1
1983        model.item(last_row, 0).setEditable(False)
1984        model.item(last_row, 4).setEditable(False)
[86f88d1]1985
1986    def addScaleToModel(self, model):
1987        """
1988        Adds scale parameter with default values to the model
1989        """
[cbcdd2c]1990        assert isinstance(model, QtGui.QStandardItemModel)
[86f88d1]1991        checked_list = ['scale', '1.0', '0.0', 'inf', '']
[4d457df]1992        FittingUtilities.addCheckedListToModel(model, checked_list)
[2add354]1993        last_row = model.rowCount()-1
1994        model.item(last_row, 0).setEditable(False)
1995        model.item(last_row, 4).setEditable(False)
[86f88d1]1996
[9d266d2]1997    def addWeightingToData(self, data):
1998        """
1999        Adds weighting contribution to fitting data
[1bc27f1]2000        """
[b764ae5]2001        new_data = copy.deepcopy(data)
[e1e3e09]2002        # Send original data for weighting
[dc5ef15]2003        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
[9a7c81c]2004        if self.is2D:
[b764ae5]2005            new_data.err_data = weight
[9a7c81c]2006        else:
[b764ae5]2007            new_data.dy = weight
2008
2009        return new_data
[9d266d2]2010
[0268aed]2011    def updateQRange(self):
2012        """
2013        Updates Q Range display
2014        """
2015        if self.data_is_loaded:
2016            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
2017        # set Q range labels on the main tab
2018        self.lblMinRangeDef.setText(str(self.q_range_min))
2019        self.lblMaxRangeDef.setText(str(self.q_range_max))
2020        # set Q range labels on the options tab
[180bd54]2021        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
[0268aed]2022
[4d457df]2023    def SASModelToQModel(self, model_name, structure_factor=None):
[60af928]2024        """
[cbcdd2c]2025        Setting model parameters into table based on selected category
[60af928]2026        """
2027        # Crete/overwrite model items
2028        self._model_model.clear()
[f3a19ad]2029        self._poly_model.clear()
2030        self._magnet_model.clear()
[5236449]2031
[f3a19ad]2032        if model_name is None:
2033            if structure_factor not in (None, "None"):
2034                # S(Q) on its own, treat the same as a form factor
2035                self.kernel_module = None
2036                self.fromStructureFactorToQModel(structure_factor)
2037            else:
2038                # No models selected
2039                return
[cd31251]2040        else:
[fd1ae6d1]2041            self.fromModelToQModel(model_name)
[70f4458]2042            self.addExtraShells()
[5236449]2043
[fd1ae6d1]2044            # Allow the SF combobox visibility for the given sasmodel
2045            self.enableStructureFactorControl(structure_factor)
[4ea8020]2046       
2047            # Add S(Q)
[605d944]2048            if self.cbStructureFactor.isEnabled():
2049                structure_factor = self.cbStructureFactor.currentText()
2050                self.fromStructureFactorToQModel(structure_factor)
[cd31251]2051
[f3a19ad]2052            # Add polydispersity to the model
[4ea8020]2053            self.poly_params = {}
[f3a19ad]2054            self.setPolyModel()
2055            # Add magnetic parameters to the model
[4ea8020]2056            self.magnet_params = {}
[f3a19ad]2057            self.setMagneticModel()
[5236449]2058
2059        # Now we claim the model has been loaded
[86f88d1]2060        self.model_is_loaded = True
[be8f4b0]2061        # Change the model name to a monicker
2062        self.kernel_module.name = self.modelName()
[9a7c81c]2063        # Update the smearing tab
2064        self.smearing_widget.updateKernelModel(kernel_model=self.kernel_module)
[86f88d1]2065
[fd1ae6d1]2066        # (Re)-create headers
2067        FittingUtilities.addHeadersToModel(self._model_model)
[6964d44]2068        self.lstParams.header().setFont(self.boldFont)
[fd1ae6d1]2069
[5236449]2070        # Update Q Ranges
2071        self.updateQRange()
2072
[fd1ae6d1]2073    def fromModelToQModel(self, model_name):
2074        """
2075        Setting model parameters into QStandardItemModel based on selected _model_
2076        """
[3b3b40b]2077        name = model_name
[e3df84e]2078        kernel_module = None
[3b3b40b]2079        if self.cbCategory.currentText() == CATEGORY_CUSTOM:
2080            # custom kernel load requires full path
2081            name = os.path.join(ModelUtilities.find_plugins_dir(), model_name+".py")
[060413c]2082        try:
2083            kernel_module = generate.load_kernel_module(name)
[e3df84e]2084        except ModuleNotFoundError as ex:
2085            pass
2086
2087        if kernel_module is None:
2088            # mismatch between "name" attribute and actual filename.
2089            curr_model = self.models[model_name]
2090            name, _ = os.path.splitext(os.path.basename(curr_model.filename))
2091            try:
2092                kernel_module = generate.load_kernel_module(name)
2093            except ModuleNotFoundError as ex:
[bb477f5]2094                logger.error("Can't find the model "+ str(ex))
[e3df84e]2095                return
[dc71408]2096
2097        if hasattr(kernel_module, 'parameters'):
2098            # built-in and custom models
2099            self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
2100
2101        elif hasattr(kernel_module, 'model_info'):
2102            # for sum/multiply models
2103            self.model_parameters = kernel_module.model_info.parameters
2104
2105        elif hasattr(kernel_module, 'Model') and hasattr(kernel_module.Model, "_model_info"):
2106            # this probably won't work if there's no model_info, but just in case
2107            self.model_parameters = kernel_module.Model._model_info.parameters
2108        else:
2109            # no parameters - default to blank table
2110            msg = "No parameters found in model '{}'.".format(model_name)
2111            logger.warning(msg)
2112            self.model_parameters = modelinfo.ParameterTable([])
[fd1ae6d1]2113
2114        # Instantiate the current sasmodel
2115        self.kernel_module = self.models[model_name]()
2116
[9ce69ec]2117        # Change the model name to a monicker
2118        self.kernel_module.name = self.modelName()
2119
[fd1ae6d1]2120        # Explicitly add scale and background with default values
[6964d44]2121        temp_undo_state = self.undo_supported
2122        self.undo_supported = False
[fd1ae6d1]2123        self.addScaleToModel(self._model_model)
2124        self.addBackgroundToModel(self._model_model)
[6964d44]2125        self.undo_supported = temp_undo_state
[fd1ae6d1]2126
[0d13814]2127        self.shell_names = self.shellNamesList()
2128
[00b7ddf0]2129        # Add heading row
2130        FittingUtilities.addHeadingRowToModel(self._model_model, model_name)
[aca8418]2131
[fd1ae6d1]2132        # Update the QModel
[04f775d]2133        FittingUtilities.addParametersToModel(
2134                self.model_parameters,
2135                self.kernel_module,
[a758043]2136                self.is2D,
2137                self._model_model,
2138                self.lstParams)
[fd1ae6d1]2139
2140    def fromStructureFactorToQModel(self, structure_factor):
2141        """
2142        Setting model parameters into QStandardItemModel based on selected _structure factor_
2143        """
[605d944]2144        if structure_factor is None or structure_factor=="None":
2145            return
[18d5c94a]2146
[01b4877]2147        product_params = None
2148
[5fb714b]2149        if self.kernel_module is None:
2150            # Structure factor is the only selected model; build it and show all its params
2151            self.kernel_module = self.models[structure_factor]()
[9ce69ec]2152            self.kernel_module.name = self.modelName()
[5fb714b]2153            s_params = self.kernel_module._model_info.parameters
2154            s_params_orig = s_params
[f3a19ad]2155        else:
[5fb714b]2156            s_kernel = self.models[structure_factor]()
2157            p_kernel = self.kernel_module
[5d1440e1]2158
[f3a19ad]2159            p_pars_len = len(p_kernel._model_info.parameters.kernel_parameters)
2160            s_pars_len = len(s_kernel._model_info.parameters.kernel_parameters)
[fd1ae6d1]2161
[f3a19ad]2162            self.kernel_module = MultiplicationModel(p_kernel, s_kernel)
[9ce69ec]2163            # Modify the name to correspond to shown items
2164            self.kernel_module.name = self.modelName()
[f3a19ad]2165            all_params = self.kernel_module._model_info.parameters.kernel_parameters
2166            all_param_names = [param.name for param in all_params]
[18d5c94a]2167
[f3a19ad]2168            # S(Q) params from the product model are not necessarily the same as those from the S(Q) model; any
[5fb714b]2169            # conflicting names with P(Q) params will cause a rename
[fd1ae6d1]2170
[f3a19ad]2171            if "radius_effective_mode" in all_param_names:
2172                # Show all parameters
[01b4877]2173                # In this case, radius_effective is NOT pruned by sasmodels.product
[f3a19ad]2174                s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len])
2175                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters)
[01b4877]2176                product_params = modelinfo.ParameterTable(
2177                        self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
[f3a19ad]2178            else:
2179                # Ensure radius_effective is not displayed
2180                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters[1:])
2181                if "radius_effective" in all_param_names:
[01b4877]2182                    # In this case, radius_effective is NOT pruned by sasmodels.product
[f3a19ad]2183                    s_params = modelinfo.ParameterTable(all_params[p_pars_len+1:p_pars_len+s_pars_len])
[01b4877]2184                    product_params = modelinfo.ParameterTable(
2185                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
[f3a19ad]2186                else:
[01b4877]2187                    # In this case, radius_effective is pruned by sasmodels.product
[f3a19ad]2188                    s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len-1])
[01b4877]2189                    product_params = modelinfo.ParameterTable(
2190                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len-1:])
[f3a19ad]2191
[00b7ddf0]2192        # Add heading row
2193        FittingUtilities.addHeadingRowToModel(self._model_model, structure_factor)
[0d72cac]2194
[b69b549]2195        # Get new rows for QModel
2196        # Any renamed parameters are stored as data in the relevant item, for later handling
[04f775d]2197        FittingUtilities.addSimpleParametersToModel(
[01b4877]2198                parameters=s_params,
2199                is2D=self.is2D,
2200                parameters_original=s_params_orig,
2201                model=self._model_model,
2202                view=self.lstParams)
2203
2204        # Insert product-only params into QModel
2205        if product_params:
2206            prod_rows = FittingUtilities.addSimpleParametersToModel(
2207                    parameters=product_params,
2208                    is2D=self.is2D,
2209                    parameters_original=None,
2210                    model=self._model_model,
2211                    view=self.lstParams,
2212                    row_num=2)
2213
2214            # Since this all happens after shells are dealt with and we've inserted rows, fix this counter
2215            self._n_shells_row += len(prod_rows)
[fd1ae6d1]2216
[6dbff18]2217    def haveParamsToFit(self):
2218        """
2219        Finds out if there are any parameters ready to be fitted
2220        """
2221        return (self.main_params_to_fit!=[]
2222                or self.poly_params_to_fit!=[]
2223                or self.magnet_params_to_fit != []) and \
2224                self.logic.data_is_loaded
2225
[b00414d]2226    def onMainParamsChange(self, item):
[cd31251]2227        """
2228        Callback method for updating the sasmodel parameters with the GUI values
2229        """
[cbcdd2c]2230        model_column = item.column()
[cd31251]2231
2232        if model_column == 0:
[f182f93]2233            self.checkboxSelected(item)
[6dbff18]2234            self.cmdFit.setEnabled(self.haveParamsToFit())
[6964d44]2235            # Update state stack
2236            self.updateUndo()
[cd31251]2237            return
2238
[f182f93]2239        model_row = item.row()
2240        name_index = self._model_model.index(model_row, 0)
[700b03b]2241        name_item = self._model_model.itemFromIndex(name_index)
[f182f93]2242
[b00414d]2243        # Extract changed value.
[2add354]2244        try:
[fbfc488]2245            value = GuiUtils.toDouble(item.text())
[0261bc1]2246        except TypeError:
[2add354]2247            # Unparsable field
2248            return
[fbfc488]2249
[700b03b]2250        # if the item has user data, this is the actual parameter name (e.g. to handle duplicate names)
2251        if name_item.data(QtCore.Qt.UserRole):
2252            parameter_name = str(name_item.data(QtCore.Qt.UserRole))
2253        else:
2254            parameter_name = str(self._model_model.data(name_index))
[cbcdd2c]2255
[00b3b40]2256        # Update the parameter value - note: this supports +/-inf as well
[cbcdd2c]2257        self.kernel_module.params[parameter_name] = value
2258
[8a32a6ff]2259        # Update the parameter value - note: this supports +/-inf as well
[8f2548c]2260        param_column = self.lstParams.itemDelegate().param_value
2261        min_column = self.lstParams.itemDelegate().param_min
2262        max_column = self.lstParams.itemDelegate().param_max
2263        if model_column == param_column:
[8a32a6ff]2264            self.kernel_module.setParam(parameter_name, value)
[8f2548c]2265        elif model_column == min_column:
[8a32a6ff]2266            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
[8f2548c]2267            self.kernel_module.details[parameter_name][1] = value
2268        elif model_column == max_column:
2269            self.kernel_module.details[parameter_name][2] = value
2270        else:
2271            # don't update the chart
2272            return
[00b3b40]2273
2274        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
2275        # TODO: multishell params in self.kernel_module.details[??] = value
[cbcdd2c]2276
[d7ff531]2277        # Force the chart update when actual parameters changed
2278        if model_column == 1:
[d48cc19]2279            self.recalculatePlotData()
[7d077d1]2280
[2241130]2281        # Update state stack
[00b3b40]2282        self.updateUndo()
[2241130]2283
[7fd20fc]2284    def isCheckable(self, row):
2285        return self._model_model.item(row, 0).isCheckable()
2286
[f182f93]2287    def checkboxSelected(self, item):
2288        # Assure we're dealing with checkboxes
2289        if not item.isCheckable():
2290            return
2291        status = item.checkState()
2292
2293        # If multiple rows selected - toggle all of them, filtering uncheckable
2294        # Switch off signaling from the model to avoid recursion
2295        self._model_model.blockSignals(True)
2296        # Convert to proper indices and set requested enablement
[7fd20fc]2297        self.setParameterSelection(status)
[f182f93]2298        self._model_model.blockSignals(False)
2299
2300        # update the list of parameters to fit
[6dbff18]2301        self.main_params_to_fit = self.checkedListFromModel(self._model_model)
[c1e380e]2302
2303    def checkedListFromModel(self, model):
2304        """
2305        Returns list of checked parameters for given model
2306        """
2307        def isChecked(row):
2308            return model.item(row, 0).checkState() == QtCore.Qt.Checked
2309
2310        return [str(model.item(row_index, 0).text())
[b3e8629]2311                for row_index in range(model.rowCount())
[c1e380e]2312                if isChecked(row_index)]
[f182f93]2313
[6fd4e36]2314    def createNewIndex(self, fitted_data):
2315        """
2316        Create a model or theory index with passed Data1D/Data2D
2317        """
2318        if self.data_is_loaded:
[0268aed]2319            if not fitted_data.name:
2320                name = self.nameForFittedData(self.data.filename)
2321                fitted_data.title = name
2322                fitted_data.name = name
2323                fitted_data.filename = name
[7d077d1]2324                fitted_data.symbol = "Line"
[6fd4e36]2325            self.updateModelIndex(fitted_data)
2326        else:
[3ae9179]2327            if not fitted_data.name:
2328                name = self.nameForFittedData(self.kernel_module.id)
2329            else:
2330                name = fitted_data.name
[0268aed]2331            fitted_data.title = name
2332            fitted_data.filename = name
2333            fitted_data.symbol = "Line"
[6fd4e36]2334            self.createTheoryIndex(fitted_data)
[5d28d6b]2335            # Switch to the theory tab for user's glee
2336            self.communicate.changeDataExplorerTabSignal.emit(1)
[6fd4e36]2337
2338    def updateModelIndex(self, fitted_data):
2339        """
2340        Update a QStandardModelIndex containing model data
2341        """
[00b3b40]2342        name = self.nameFromData(fitted_data)
[0268aed]2343        # Make this a line if no other defined
[7d077d1]2344        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
[0268aed]2345            fitted_data.symbol = 'Line'
[6fd4e36]2346        # Notify the GUI manager so it can update the main model in DataExplorer
[d4dac80]2347        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
[6fd4e36]2348
2349    def createTheoryIndex(self, fitted_data):
2350        """
2351        Create a QStandardModelIndex containing model data
2352        """
[00b3b40]2353        name = self.nameFromData(fitted_data)
2354        # Notify the GUI manager so it can create the theory model in DataExplorer
[cb90b65]2355        self.theory_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
2356        self.communicate.updateTheoryFromPerspectiveSignal.emit(self.theory_item)
[00b3b40]2357
2358    def nameFromData(self, fitted_data):
2359        """
2360        Return name for the dataset. Terribly impure function.
2361        """
[0268aed]2362        if fitted_data.name is None:
[00b3b40]2363            name = self.nameForFittedData(self.logic.data.filename)
[0268aed]2364            fitted_data.title = name
2365            fitted_data.name = name
2366            fitted_data.filename = name
2367        else:
2368            name = fitted_data.name
[00b3b40]2369        return name
[5236449]2370
[4d457df]2371    def methodCalculateForData(self):
2372        '''return the method for data calculation'''
2373        return Calc1D if isinstance(self.data, Data1D) else Calc2D
2374
2375    def methodCompleteForData(self):
2376        '''return the method for result parsin on calc complete '''
[d4dac80]2377        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
[4d457df]2378
[66d4370]2379    def updateKernelModelWithExtraParams(self, model=None):
2380        """
2381        Updates kernel model 'model' with extra parameters from
2382        the polydisp and magnetism tab, if the tabs are enabled
2383        """
2384        if model is None: return
2385        if not hasattr(model, 'setParam'): return
2386
2387        # add polydisperse parameters if asked
[2d47985]2388        if self.chkPolydispersity.isChecked() and self._poly_model.rowCount() > 0:
[66d4370]2389            for key, value in self.poly_params.items():
2390                model.setParam(key, value)
2391        # add magnetic params if asked
2392        if self.chkMagnetism.isChecked():
[2d47985]2393            for key, value in self.magnet_params.items() and self._magnet_model.rowCount() > 0:
[66d4370]2394                model.setParam(key, value)
2395
[d4dac80]2396    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
[86f88d1]2397        """
[d4dac80]2398        Wrapper for Calc1D/2D calls
[86f88d1]2399        """
[d4dac80]2400        if data is None:
2401            data = self.data
2402        if model is None:
[66d4370]2403            model = copy.deepcopy(self.kernel_module)
2404            self.updateKernelModelWithExtraParams(model)
2405
[d4dac80]2406        if completefn is None:
2407            completefn = self.methodCompleteForData()
[9a7c81c]2408        smearer = self.smearing_widget.smearer()
[b764ae5]2409        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2410
[557fc498]2411        # Disable buttons/table
2412        self.disableInteractiveElements()
[4d457df]2413        # Awful API to a backend method.
[d4dac80]2414        calc_thread = self.methodCalculateForData()(data=data,
2415                                               model=model,
[1bc27f1]2416                                               page_id=0,
2417                                               qmin=self.q_range_min,
2418                                               qmax=self.q_range_max,
[9a7c81c]2419                                               smearer=smearer,
[1bc27f1]2420                                               state=None,
[b764ae5]2421                                               weight=weight,
[1bc27f1]2422                                               fid=None,
2423                                               toggle_mode_on=False,
[d4dac80]2424                                               completefn=completefn,
[1bc27f1]2425                                               update_chisqr=True,
2426                                               exception_handler=self.calcException,
2427                                               source=None)
[d4dac80]2428        if use_threads:
2429            if LocalConfig.USING_TWISTED:
2430                # start the thread with twisted
2431                thread = threads.deferToThread(calc_thread.compute)
2432                thread.addCallback(completefn)
2433                thread.addErrback(self.calculateDataFailed)
2434            else:
2435                # Use the old python threads + Queue
2436                calc_thread.queue()
2437                calc_thread.ready(2.5)
2438        else:
2439            results = calc_thread.compute()
2440            completefn(results)
[4d457df]2441
[d4dac80]2442    def calculateQGridForModel(self):
2443        """
2444        Prepare the fitting data object, based on current ModelModel
2445        """
2446        if self.kernel_module is None:
2447            return
2448        self.calculateQGridForModelExt()
[6964d44]2449
[aca8418]2450    def calculateDataFailed(self, reason):
[6964d44]2451        """
[c1e380e]2452        Thread returned error
[6964d44]2453        """
[557fc498]2454        # Bring the GUI to normal state
2455        self.enableInteractiveElements()
[b3e8629]2456        print("Calculate Data failed with ", reason)
[5236449]2457
[d4dac80]2458    def completed1D(self, return_data):
2459        self.Calc1DFinishedSignal.emit(return_data)
2460
2461    def completed2D(self, return_data):
2462        self.Calc2DFinishedSignal.emit(return_data)
2463
[cbcdd2c]2464    def complete1D(self, return_data):
[5236449]2465        """
[4d457df]2466        Plot the current 1D data
2467        """
[557fc498]2468        # Bring the GUI to normal state
2469        self.enableInteractiveElements()
[9ce69ec]2470        if return_data is None:
2471            self.calculateDataFailed("Results not available.")
2472            return
[d48cc19]2473        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
[d1e4689]2474        if len(fitted_data.x) != len(self.data.x):
2475            return
2476
[0fe7e5b]2477        residuals = self.calculateResiduals(fitted_data)
[d48cc19]2478        self.model_data = fitted_data
[5aad7a5]2479        new_plots = [fitted_data]
2480        if residuals is not None:
2481            new_plots.append(residuals)
[cbcdd2c]2482
[fd7ef36]2483        if self.data_is_loaded:
[0dcb71d]2484            # delete any plots associated with the data that were not updated (e.g. to remove beta(Q), S_eff(Q))
2485            GuiUtils.deleteRedundantPlots(self.all_data[self.data_index], new_plots)
[5b144c6]2486            pass
[fd7ef36]2487        else:
2488            # delete theory items for the model, in order to get rid of any redundant items, e.g. beta(Q), S_eff(Q)
2489            self.communicate.deleteIntermediateTheoryPlotsSignal.emit(self.kernel_module.id)
[0fe7e5b]2490
[3ae9179]2491        # Create plots for intermediate product data
[40975f8]2492        plots = self.logic.new1DProductPlots(return_data, self.tab_id)
2493        for plot in plots:
2494            plot.symbol = "Line"
2495            self.createNewIndex(plot)
2496            new_plots.append(plot)
[0fe7e5b]2497
[9463ca2]2498        for plot in new_plots:
[5aad7a5]2499            self.communicate.plotUpdateSignal.emit([plot])
[9463ca2]2500
[cbcdd2c]2501    def complete2D(self, return_data):
2502        """
[4d457df]2503        Plot the current 2D data
2504        """
[557fc498]2505        # Bring the GUI to normal state
2506        self.enableInteractiveElements()
2507
[6fd4e36]2508        fitted_data = self.logic.new2DPlot(return_data)
[66d4370]2509        residuals = self.calculateResiduals(fitted_data)
[d48cc19]2510        self.model_data = fitted_data
[66d4370]2511        new_plots = [fitted_data]
2512        if residuals is not None:
2513            new_plots.append(residuals)
2514
2515        # Update/generate plots
2516        for plot in new_plots:
2517            self.communicate.plotUpdateSignal.emit([plot])
[6fd4e36]2518
2519    def calculateResiduals(self, fitted_data):
2520        """
[9463ca2]2521        Calculate and print Chi2 and display chart of residuals. Returns residuals plot object.
[6fd4e36]2522        """
2523        # Create a new index for holding data
[7d077d1]2524        fitted_data.symbol = "Line"
[6964d44]2525
2526        # Modify fitted_data with weighting
[b764ae5]2527        weighted_data = self.addWeightingToData(fitted_data)
[6964d44]2528
[b764ae5]2529        self.createNewIndex(weighted_data)
[6fd4e36]2530        # Calculate difference between return_data and logic.data
[b764ae5]2531        self.chi2 = FittingUtilities.calculateChi2(weighted_data, self.logic.data)
[6fd4e36]2532        # Update the control
[2add354]2533        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
[f182f93]2534        self.lblChi2Value.setText(chi2_repr)
[cbcdd2c]2535
[0268aed]2536        # Plot residuals if actual data
[aca8418]2537        if not self.data_is_loaded:
2538            return
2539
[b764ae5]2540        residuals_plot = FittingUtilities.plotResiduals(self.data, weighted_data)
[aca8418]2541        residuals_plot.id = "Residual " + residuals_plot.id
[a54bbf2b]2542        residuals_plot.plot_role = Data1D.ROLE_RESIDUAL
[aca8418]2543        self.createNewIndex(residuals_plot)
[0fe7e5b]2544        return residuals_plot
[5236449]2545
[3d18691]2546    def onCategoriesChanged(self):
2547            """
2548            Reload the category/model comboboxes
2549            """
2550            # Store the current combo indices
2551            current_cat = self.cbCategory.currentText()
2552            current_model = self.cbModel.currentText()
2553
2554            # reread the category file and repopulate the combo
2555            self.cbCategory.blockSignals(True)
2556            self.cbCategory.clear()
2557            self.readCategoryInfo()
2558            self.initializeCategoryCombo()
2559
2560            # Scroll back to the original index in Categories
2561            new_index = self.cbCategory.findText(current_cat)
2562            if new_index != -1:
2563                self.cbCategory.setCurrentIndex(new_index)
2564            self.cbCategory.blockSignals(False)
2565            # ...and in the Models
2566            self.cbModel.blockSignals(True)
2567            new_index = self.cbModel.findText(current_model)
2568            if new_index != -1:
2569                self.cbModel.setCurrentIndex(new_index)
2570            self.cbModel.blockSignals(False)
2571
2572            return
2573
[5236449]2574    def calcException(self, etype, value, tb):
2575        """
[c1e380e]2576        Thread threw an exception.
[5236449]2577        """
[557fc498]2578        # Bring the GUI to normal state
2579        self.enableInteractiveElements()
[c1e380e]2580        # TODO: remimplement thread cancellation
[bb477f5]2581        logger.error("".join(traceback.format_exception(etype, value, tb)))
[60af928]2582
2583    def setTableProperties(self, table):
2584        """
2585        Setting table properties
2586        """
2587        # Table properties
2588        table.verticalHeader().setVisible(False)
2589        table.setAlternatingRowColors(True)
[4992ff2]2590        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2591        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
[f46f6dc]2592        table.resizeColumnsToContents()
2593
[60af928]2594        # Header
2595        header = table.horizontalHeader()
[4992ff2]2596        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2597        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
[f46f6dc]2598
[4992ff2]2599        # Qt5: the following 2 lines crash - figure out why!
[e43fc91]2600        # Resize column 0 and 7 to content
[4992ff2]2601        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2602        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
[60af928]2603
2604    def setPolyModel(self):
2605        """
2606        Set polydispersity values
2607        """
[86f88d1]2608        if not self.model_parameters:
2609            return
2610        self._poly_model.clear()
2611
[f53bc04]2612        parameters = self.model_parameters.form_volume_parameters
2613        if self.is2D:
2614            parameters += self.model_parameters.orientation_parameters
2615
[e43fc91]2616        [self.setPolyModelParameters(i, param) for i, param in \
[f53bc04]2617            enumerate(parameters) if param.polydisperse]
2618
[4d457df]2619        FittingUtilities.addPolyHeadersToModel(self._poly_model)
[60af928]2620
[e43fc91]2621    def setPolyModelParameters(self, i, param):
[aca8418]2622        """
[0d13814]2623        Standard of multishell poly parameter driver
[aca8418]2624        """
[0d13814]2625        param_name = param.name
2626        # see it the parameter is multishell
[06b0138]2627        if '[' in param.name:
[0d13814]2628            # Skip empty shells
2629            if self.current_shell_displayed == 0:
2630                return
2631            else:
2632                # Create as many entries as current shells
[b3e8629]2633                for ishell in range(1, self.current_shell_displayed+1):
[0d13814]2634                    # Remove [n] and add the shell numeral
2635                    name = param_name[0:param_name.index('[')] + str(ishell)
[e43fc91]2636                    self.addNameToPolyModel(i, name)
[0d13814]2637        else:
2638            # Just create a simple param entry
[e43fc91]2639            self.addNameToPolyModel(i, param_name)
[0d13814]2640
[e43fc91]2641    def addNameToPolyModel(self, i, param_name):
[0d13814]2642        """
2643        Creates a checked row in the poly model with param_name
2644        """
[144ec831]2645        # Polydisp. values from the sasmodel
[0d13814]2646        width = self.kernel_module.getParam(param_name + '.width')
2647        npts = self.kernel_module.getParam(param_name + '.npts')
2648        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2649        _, min, max = self.kernel_module.details[param_name]
[144ec831]2650
[66d4370]2651        # Update local param dict
2652        self.poly_params[param_name + '.width'] = width
2653        self.poly_params[param_name + '.npts'] = npts
2654        self.poly_params[param_name + '.nsigmas'] = nsigs
2655
[144ec831]2656        # Construct a row with polydisp. related variable.
2657        # This will get added to the polydisp. model
2658        # Note: last argument needs extra space padding for decent display of the control
[0d13814]2659        checked_list = ["Distribution of " + param_name, str(width),
2660                        str(min), str(max),
[e43fc91]2661                        str(npts), str(nsigs), "gaussian      ",'']
[aca8418]2662        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2663
2664        # All possible polydisp. functions as strings in combobox
[4992ff2]2665        func = QtWidgets.QComboBox()
[b3e8629]2666        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
[e43fc91]2667        # Set the default index
[aca8418]2668        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
[e43fc91]2669        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2670        self.lstPoly.setIndexWidget(ind, func)
2671        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2672
2673    def onPolyFilenameChange(self, row_index):
2674        """
2675        Respond to filename_updated signal from the delegate
2676        """
2677        # For the given row, invoke the "array" combo handler
2678        array_caption = 'array'
[8222f171]2679
[e43fc91]2680        # Get the combo box reference
2681        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2682        widget = self.lstPoly.indexWidget(ind)
[8222f171]2683
[e43fc91]2684        # Update the combo box so it displays "array"
2685        widget.blockSignals(True)
2686        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
2687        widget.blockSignals(False)
[aca8418]2688
[8222f171]2689        # Invoke the file reader
2690        self.onPolyComboIndexChange(array_caption, row_index)
2691
[aca8418]2692    def onPolyComboIndexChange(self, combo_string, row_index):
2693        """
2694        Modify polydisp. defaults on function choice
2695        """
[144ec831]2696        # Get npts/nsigs for current selection
[aca8418]2697        param = self.model_parameters.form_volume_parameters[row_index]
[e43fc91]2698        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2699        combo_box = self.lstPoly.indexWidget(file_index)
[aca8418]2700
[919d47c]2701        def updateFunctionCaption(row):
2702            # Utility function for update of polydispersity function name in the main model
[00b7ddf0]2703            if not self.isCheckable(row):
2704                return
[73665a8]2705            self._model_model.blockSignals(True)
[1643d8ed]2706            param_name = str(self._model_model.item(row, 0).text())
[73665a8]2707            self._model_model.blockSignals(False)
[919d47c]2708            if param_name !=  param.name:
2709                return
[144ec831]2710            # Modify the param value
[73665a8]2711            self._model_model.blockSignals(True)
[906e0c7]2712            if self.has_error_column:
2713                # err column changes the indexing
2714                self._model_model.item(row, 0).child(0).child(0,5).setText(combo_string)
2715            else:
2716                self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
[73665a8]2717            self._model_model.blockSignals(False)
[919d47c]2718
[aca8418]2719        if combo_string == 'array':
2720            try:
[e43fc91]2721                self.loadPolydispArray(row_index)
[919d47c]2722                # Update main model for display
2723                self.iterateOverModel(updateFunctionCaption)
[e43fc91]2724                # disable the row
2725                lo = self.lstPoly.itemDelegate().poly_pd
2726                hi = self.lstPoly.itemDelegate().poly_function
[b3e8629]2727                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
[aca8418]2728                return
[e43fc91]2729            except IOError:
[8222f171]2730                combo_box.setCurrentIndex(self.orig_poly_index)
[e43fc91]2731                # Pass for cancel/bad read
2732                pass
[aca8418]2733
2734        # Enable the row in case it was disabled by Array
[919d47c]2735        self._poly_model.blockSignals(True)
[e43fc91]2736        max_range = self.lstPoly.itemDelegate().poly_filename
[b3e8629]2737        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
[e43fc91]2738        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
[b3e8629]2739        self._poly_model.setData(file_index, "")
[919d47c]2740        self._poly_model.blockSignals(False)
[aca8418]2741
[8eaa101]2742        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
2743        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
[aca8418]2744
2745        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
2746        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
2747
[b3e8629]2748        self._poly_model.setData(npts_index, npts)
2749        self._poly_model.setData(nsigs_index, nsigs)
[aca8418]2750
[919d47c]2751        self.iterateOverModel(updateFunctionCaption)
[8222f171]2752        self.orig_poly_index = combo_box.currentIndex()
[919d47c]2753
[e43fc91]2754    def loadPolydispArray(self, row_index):
[aca8418]2755        """
2756        Show the load file dialog and loads requested data into state
2757        """
[4992ff2]2758        datafile = QtWidgets.QFileDialog.getOpenFileName(
2759            self, "Choose a weight file", "", "All files (*.*)", None,
[fbfc488]2760            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
[72f4834]2761
[fbfc488]2762        if not datafile:
[bb477f5]2763            logger.info("No weight data chosen.")
[1643d8ed]2764            raise IOError
[72f4834]2765
[aca8418]2766        values = []
2767        weights = []
[919d47c]2768        def appendData(data_tuple):
2769            """
2770            Fish out floats from a tuple of strings
2771            """
2772            try:
2773                values.append(float(data_tuple[0]))
2774                weights.append(float(data_tuple[1]))
2775            except (ValueError, IndexError):
2776                # just pass through if line with bad data
2777                return
2778
[aca8418]2779        with open(datafile, 'r') as column_file:
2780            column_data = [line.rstrip().split() for line in column_file.readlines()]
[919d47c]2781            [appendData(line) for line in column_data]
[aca8418]2782
[1643d8ed]2783        # If everything went well - update the sasmodel values
[aca8418]2784        self.disp_model = POLYDISPERSITY_MODELS['array']()
2785        self.disp_model.set_weights(np.array(values), np.array(weights))
[e43fc91]2786        # + update the cell with filename
2787        fname = os.path.basename(str(datafile))
2788        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
[b3e8629]2789        self._poly_model.setData(fname_index, fname)
[aca8418]2790
[f37cab0]2791    def onColumnWidthUpdate(self, index, old_size, new_size):
2792        """
2793        Simple state update of the current column widths in the  param list
2794        """
2795        self.lstParamHeaderSizes[index] = new_size
2796
[60af928]2797    def setMagneticModel(self):
2798        """
2799        Set magnetism values on model
2800        """
[86f88d1]2801        if not self.model_parameters:
2802            return
2803        self._magnet_model.clear()
[aca8418]2804        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
[06b0138]2805            self.model_parameters.call_parameters if param.type == 'magnetic']
[4d457df]2806        FittingUtilities.addHeadersToModel(self._magnet_model)
[60af928]2807
[0d13814]2808    def shellNamesList(self):
2809        """
2810        Returns list of names of all multi-shell parameters
2811        E.g. for sld[n], radius[n], n=1..3 it will return
2812        [sld1, sld2, sld3, radius1, radius2, radius3]
2813        """
2814        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
2815        top_index = self.kernel_module.multiplicity_info.number
2816        shell_names = []
[b3e8629]2817        for i in range(1, top_index+1):
[0d13814]2818            for name in multi_names:
2819                shell_names.append(name+str(i))
2820        return shell_names
2821
[aca8418]2822    def addCheckedMagneticListToModel(self, param, model):
2823        """
2824        Wrapper for model update with a subset of magnetic parameters
2825        """
[0d13814]2826        if param.name[param.name.index(':')+1:] in self.shell_names:
2827            # check if two-digit shell number
2828            try:
2829                shell_index = int(param.name[-2:])
2830            except ValueError:
2831                shell_index = int(param.name[-1:])
2832
2833            if shell_index > self.current_shell_displayed:
2834                return
2835
[aca8418]2836        checked_list = [param.name,
2837                        str(param.default),
2838                        str(param.limits[0]),
2839                        str(param.limits[1]),
2840                        param.units]
2841
[66d4370]2842        self.magnet_params[param.name] = param.default
2843
[aca8418]2844        FittingUtilities.addCheckedListToModel(model, checked_list)
2845
[fd1ae6d1]2846    def enableStructureFactorControl(self, structure_factor):
[cd31251]2847        """
2848        Add structure factors to the list of parameters
2849        """
[fd1ae6d1]2850        if self.kernel_module.is_form_factor or structure_factor == 'None':
[cd31251]2851            self.enableStructureCombo()
2852        else:
2853            self.disableStructureCombo()
2854
[60af928]2855    def addExtraShells(self):
2856        """
[f46f6dc]2857        Add a combobox for multiple shell display
[60af928]2858        """
[4d457df]2859        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
[f46f6dc]2860
2861        if param_length == 0:
2862            return
2863
[6f7f652]2864        # cell 1: variable name
[f46f6dc]2865        item1 = QtGui.QStandardItem(param_name)
2866
[4992ff2]2867        func = QtWidgets.QComboBox()
[60af928]2868
[6f7f652]2869        # cell 2: combobox
[f46f6dc]2870        item2 = QtGui.QStandardItem()
[13dd7d2]2871
2872        # cell 3: min value
2873        item3 = QtGui.QStandardItem()
2874
2875        # cell 4: max value
2876        item4 = QtGui.QStandardItem()
2877
[86d3207]2878        # cell 4: SLD button
2879        item5 = QtGui.QStandardItem()
2880        button = QtWidgets.QPushButton()
2881        button.setText("Show SLD Profile")
2882
2883        self._model_model.appendRow([item1, item2, item3, item4, item5])
[60af928]2884
[6f7f652]2885        # Beautify the row:  span columns 2-4
[60af928]2886        shell_row = self._model_model.rowCount()
[f46f6dc]2887        shell_index = self._model_model.index(shell_row-1, 1)
[86d3207]2888        button_index = self._model_model.index(shell_row-1, 4)
[86f88d1]2889
[4d457df]2890        self.lstParams.setIndexWidget(shell_index, func)
[86d3207]2891        self.lstParams.setIndexWidget(button_index, button)
[70f4458]2892        self._n_shells_row = shell_row - 1
[86f88d1]2893
[f712bf30]2894        # Get the default number of shells for the model
2895        kernel_pars = self.kernel_module._model_info.parameters.kernel_parameters
2896        shell_par = None
2897        for par in kernel_pars:
2898            if par.name == param_name:
2899                shell_par = par
2900                break
2901        if not shell_par:
2902            logger.error("Could not find %s in kernel parameters.", param_name)
2903        default_shell_count = shell_par.default
[13dd7d2]2904        shell_min = 0
2905        shell_max = 0
2906        try:
2907            shell_min = int(shell_par.limits[0])
2908            shell_max = int(shell_par.limits[1])
2909        except IndexError as ex:
2910            # no info about limits
2911            pass
2912        item3.setText(str(shell_min))
2913        item4.setText(str(shell_max))
2914
2915        # Respond to index change
2916        func.currentTextChanged.connect(self.modifyShellsInList)
2917
[86d3207]2918        # Respond to button press
2919        button.clicked.connect(self.onShowSLDProfile)
2920
[13dd7d2]2921        # Available range of shells displayed in the combobox
2922        func.addItems([str(i) for i in range(shell_min, shell_max+1)])
[f712bf30]2923
2924        # Add default number of shells to the model
[13dd7d2]2925        func.setCurrentText(str(default_shell_count))
[a9b568c]2926
[13dd7d2]2927    def modifyShellsInList(self, text):
[86f88d1]2928        """
2929        Add/remove additional multishell parameters
2930        """
2931        # Find row location of the combobox
[70f4458]2932        first_row = self._n_shells_row + 1
2933        remove_rows = self._num_shell_params
[13dd7d2]2934        try:
2935            index = int(text)
2936        except ValueError:
2937            # bad text on the control!
2938            index = 0
2939            logger.error("Multiplicity incorrect! Setting to 0")
[86d3207]2940        self.kernel_module.multiplicity = index
[86f88d1]2941        if remove_rows > 1:
[70f4458]2942            self._model_model.removeRows(first_row, remove_rows)
2943
[b69b549]2944        new_rows = FittingUtilities.addShellsToModel(
[88ada06]2945                self.model_parameters,
2946                self._model_model,
[a758043]2947                index,
[b69b549]2948                first_row,
[a758043]2949                self.lstParams)
[86f88d1]2950
[b69b549]2951        self._num_shell_params = len(new_rows)
[a9b568c]2952        self.current_shell_displayed = index
[60af928]2953
[daf7c9c]2954        # Param values for existing shells were reset to default; force all changes into kernel module
2955        for row in new_rows:
2956            par = row[0].text()
2957            val = GuiUtils.toDouble(row[1].text())
2958            self.kernel_module.setParam(par, val)
2959
2960        # Change 'n' in the parameter model; also causes recalculation
[3090270]2961        self._model_model.item(self._n_shells_row, 1).setText(str(index))
2962
[0d13814]2963        # Update relevant models
2964        self.setPolyModel()
2965        self.setMagneticModel()
2966
[86d3207]2967    def onShowSLDProfile(self):
2968        """
2969        Show a quick plot of SLD profile
2970        """
2971        # get profile data
2972        x, y = self.kernel_module.getProfile()
2973        y *= 1.0e6
2974        profile_data = Data1D(x=x, y=y)
2975        profile_data.name = "SLD"
2976        profile_data.scale = 'linear'
2977        profile_data.symbol = 'Line'
2978        profile_data.hide_error = True
2979        profile_data._xaxis = "R(\AA)"
2980        profile_data._yaxis = "SLD(10^{-6}\AA^{-2})"
2981
2982        plotter = PlotterWidget(self, quickplot=True)
2983        plotter.data = profile_data
2984        plotter.showLegend = True
2985        plotter.plot(hide_error=True, marker='-')
2986
2987        self.plot_widget = QtWidgets.QWidget()
2988        self.plot_widget.setWindowTitle("Scattering Length Density Profile")
2989        layout = QtWidgets.QVBoxLayout()
2990        layout.addWidget(plotter)
2991        self.plot_widget.setLayout(layout)
2992        self.plot_widget.show()
2993
[557fc498]2994    def setInteractiveElements(self, enabled=True):
[14ec91c5]2995        """
[557fc498]2996        Switch interactive GUI elements on/off
[14ec91c5]2997        """
[557fc498]2998        assert isinstance(enabled, bool)
2999
3000        self.lstParams.setEnabled(enabled)
3001        self.lstPoly.setEnabled(enabled)
3002        self.lstMagnetic.setEnabled(enabled)
3003
3004        self.cbCategory.setEnabled(enabled)
3005        self.cbModel.setEnabled(enabled)
3006        self.cbStructureFactor.setEnabled(enabled)
[14ec91c5]3007
[557fc498]3008        self.chkPolydispersity.setEnabled(enabled)
3009        self.chkMagnetism.setEnabled(enabled)
3010        self.chk2DView.setEnabled(enabled)
3011
3012    def enableInteractiveElements(self):
[14ec91c5]3013        """
[557fc498]3014        Set buttion caption on fitting/calculate finish
3015        Enable the param table(s)
[14ec91c5]3016        """
[ded5e77]3017        # Notify the user that fitting is available
3018        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
[14ec91c5]3019        self.cmdFit.setText("Fit")
[ded5e77]3020        self.fit_started = False
[557fc498]3021        self.setInteractiveElements(True)
3022
3023    def disableInteractiveElements(self):
3024        """
3025        Set buttion caption on fitting/calculate start
3026        Disable the param table(s)
3027        """
3028        # Notify the user that fitting is being run
3029        # Allow for stopping the job
3030        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
3031        self.cmdFit.setText('Stop fit')
3032        self.setInteractiveElements(False)
[14ec91c5]3033
[672b8ab]3034    def readFitPage(self, fp):
3035        """
3036        Read in state from a fitpage object and update GUI
3037        """
3038        assert isinstance(fp, FitPage)
3039        # Main tab info
3040        self.logic.data.filename = fp.filename
3041        self.data_is_loaded = fp.data_is_loaded
3042        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
3043        self.chkMagnetism.setCheckState(fp.is_magnetic)
3044        self.chk2DView.setCheckState(fp.is2D)
3045
3046        # Update the comboboxes
3047        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
3048        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
3049        if fp.current_factor:
3050            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
3051
3052        self.chi2 = fp.chi2
3053
3054        # Options tab
3055        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
3056        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
3057        self.npts = fp.fit_options[fp.NPTS]
3058        self.log_points = fp.fit_options[fp.LOG_POINTS]
3059        self.weighting = fp.fit_options[fp.WEIGHTING]
3060
3061        # Models
[d60da0c]3062        self._model_model = fp.model_model
3063        self._poly_model = fp.poly_model
3064        self._magnet_model = fp.magnetism_model
[672b8ab]3065
3066        # Resolution tab
3067        smearing = fp.smearing_options[fp.SMEARING_OPTION]
3068        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
3069        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
3070        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
3071        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
3072
3073        # TODO: add polidyspersity and magnetism
3074
3075    def saveToFitPage(self, fp):
3076        """
3077        Write current state to the given fitpage
3078        """
3079        assert isinstance(fp, FitPage)
3080
3081        # Main tab info
3082        fp.filename = self.logic.data.filename
3083        fp.data_is_loaded = self.data_is_loaded
3084        fp.is_polydisperse = self.chkPolydispersity.isChecked()
3085        fp.is_magnetic = self.chkMagnetism.isChecked()
3086        fp.is2D = self.chk2DView.isChecked()
3087        fp.data = self.data
3088
3089        # Use current models - they contain all the required parameters
3090        fp.model_model = self._model_model
3091        fp.poly_model = self._poly_model
3092        fp.magnetism_model = self._magnet_model
3093
3094        if self.cbCategory.currentIndex() != 0:
3095            fp.current_category = str(self.cbCategory.currentText())
3096            fp.current_model = str(self.cbModel.currentText())
3097
3098        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
3099            fp.current_factor = str(self.cbStructureFactor.currentText())
3100        else:
3101            fp.current_factor = ''
3102
3103        fp.chi2 = self.chi2
[6dbff18]3104        fp.main_params_to_fit = self.main_params_to_fit
3105        fp.poly_params_to_fit = self.poly_params_to_fit
3106        fp.magnet_params_to_fit = self.magnet_params_to_fit
[6964d44]3107        fp.kernel_module = self.kernel_module
[672b8ab]3108
[6ff2eb3]3109        # Algorithm options
3110        # fp.algorithm = self.parent.fit_options.selected_id
3111
[672b8ab]3112        # Options tab
3113        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
3114        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
3115        fp.fit_options[fp.NPTS] = self.npts
3116        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
3117        fp.fit_options[fp.LOG_POINTS] = self.log_points
3118        fp.fit_options[fp.WEIGHTING] = self.weighting
3119
3120        # Resolution tab
3121        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3122        fp.smearing_options[fp.SMEARING_OPTION] = smearing
3123        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
3124        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
3125        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
3126
3127        # TODO: add polidyspersity and magnetism
3128
[00b3b40]3129    def updateUndo(self):
3130        """
3131        Create a new state page and add it to the stack
3132        """
3133        if self.undo_supported:
3134            self.pushFitPage(self.currentState())
3135
[672b8ab]3136    def currentState(self):
3137        """
3138        Return fit page with current state
3139        """
3140        new_page = FitPage()
3141        self.saveToFitPage(new_page)
3142
3143        return new_page
3144
3145    def pushFitPage(self, new_page):
3146        """
3147        Add a new fit page object with current state
3148        """
[6011788]3149        self.page_stack.append(new_page)
[672b8ab]3150
3151    def popFitPage(self):
3152        """
3153        Remove top fit page from stack
3154        """
[6011788]3155        if self.page_stack:
3156            self.page_stack.pop()
[672b8ab]3157
[57be490]3158    def getReport(self):
3159        """
3160        Create and return HTML report with parameters and charts
3161        """
3162        index = None
3163        if self.all_data:
3164            index = self.all_data[self.data_index]
[cb90b65]3165        else:
3166            index = self.theory_item
[57be490]3167        report_logic = ReportPageLogic(self,
3168                                       kernel_module=self.kernel_module,
3169                                       data=self.data,
3170                                       index=index,
3171                                       model=self._model_model)
3172
3173        return report_logic.reportList()
3174
3175    def savePageState(self):
3176        """
3177        Create and serialize local PageState
3178        """
3179        from sas.sascalc.fit.pagestate import Reader
3180        model = self.kernel_module
3181
3182        # Old style PageState object
3183        state = PageState(model=model, data=self.data)
3184
3185        # Add parameter data to the state
3186        self.getCurrentFitState(state)
3187
3188        # Create the filewriter, aptly named 'Reader'
3189        state_reader = Reader(self.loadPageStateCallback)
3190        filepath = self.saveAsAnalysisFile()
[10fee37]3191        if filepath is None or filepath == "":
[57be490]3192            return
3193        state_reader.write(filename=filepath, fitstate=state)
3194        pass
3195
3196    def saveAsAnalysisFile(self):
3197        """
3198        Show the save as... dialog and return the chosen filepath
3199        """
3200        default_name = "FitPage"+str(self.tab_id)+".fitv"
3201
3202        wildcard = "fitv files (*.fitv)"
3203        kwargs = {
3204            'caption'   : 'Save As',
3205            'directory' : default_name,
3206            'filter'    : wildcard,
3207            'parent'    : None,
3208        }
3209        # Query user for filename.
3210        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
3211        filename = filename_tuple[0]
3212        return filename
3213
3214    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
3215        """
3216        This is a callback method called from the CANSAS reader.
3217        We need the instance of this reader only for writing out a file,
3218        so there's nothing here.
3219        Until Load Analysis is implemented, that is.
3220        """
3221        pass
3222
3223    def loadPageState(self, pagestate=None):
3224        """
3225        Load the PageState object and update the current widget
3226        """
3227        pass
3228
3229    def getCurrentFitState(self, state=None):
3230        """
3231        Store current state for fit_page
3232        """
3233        # save model option
3234        #if self.model is not None:
3235        #    self.disp_list = self.getDispParamList()
3236        #    state.disp_list = copy.deepcopy(self.disp_list)
3237        #    #state.model = self.model.clone()
3238
3239        # Comboboxes
3240        state.categorycombobox = self.cbCategory.currentText()
3241        state.formfactorcombobox = self.cbModel.currentText()
3242        if self.cbStructureFactor.isEnabled():
[10fee37]3243            state.structurecombobox = self.cbStructureFactor.currentText()
[57be490]3244        state.tcChi = self.chi2
3245
3246        state.enable2D = self.is2D
3247
3248        #state.weights = copy.deepcopy(self.weights)
3249        # save data
3250        state.data = copy.deepcopy(self.data)
3251
3252        # save plotting range
3253        state.qmin = self.q_range_min
3254        state.qmax = self.q_range_max
3255        state.npts = self.npts
3256
3257        #    self.state.enable_disp = self.enable_disp.GetValue()
3258        #    self.state.disable_disp = self.disable_disp.GetValue()
3259
3260        #    self.state.enable_smearer = \
3261        #                        copy.deepcopy(self.enable_smearer.GetValue())
3262        #    self.state.disable_smearer = \
3263        #                        copy.deepcopy(self.disable_smearer.GetValue())
3264
3265        #self.state.pinhole_smearer = \
3266        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
3267        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
3268        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
3269        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
3270        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
3271        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
3272
3273        p = self.model_parameters
3274        # save checkbutton state and txtcrtl values
[10fee37]3275        state.parameters = FittingUtilities.getStandardParam(self._model_model)
3276        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
[57be490]3277
3278        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
3279        #self._copy_parameters_state(self.parameters, self.state.parameters)
3280        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
3281        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
3282
[8e2cd79]3283    def onParameterCopy(self, format=None):
3284        """
3285        Copy current parameters into the clipboard
3286        """
3287        # run a loop over all parameters and pull out
3288        # first - regular params
3289        param_list = []
[0eff615]3290
3291        param_list.append(['model_name', str(self.cbModel.currentText())])
[8e2cd79]3292        def gatherParams(row):
3293            """
3294            Create list of main parameters based on _model_model
3295            """
3296            param_name = str(self._model_model.item(row, 0).text())
3297            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3298            param_value = str(self._model_model.item(row, 1).text())
3299            param_error = None
[685602a]3300            param_min = None
3301            param_max = None
[8e2cd79]3302            column_offset = 0
3303            if self.has_error_column:
3304                param_error = str(self._model_model.item(row, 2).text())
3305                column_offset = 1
[685602a]3306
3307            try:
3308                param_min = str(self._model_model.item(row, 2+column_offset).text())
3309                param_max = str(self._model_model.item(row, 3+column_offset).text())
3310            except:
3311                pass
3312
[8e2cd79]3313            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3314
3315        def gatherPolyParams(row):
3316            """
3317            Create list of polydisperse parameters based on _poly_model
3318            """
3319            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
3320            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3321            param_value = str(self._poly_model.item(row, 1).text())
3322            param_error = None
3323            column_offset = 0
3324            if self.has_poly_error_column:
3325                param_error = str(self._poly_model.item(row, 2).text())
3326                column_offset = 1
3327            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
3328            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
3329            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
3330            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
3331            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
3332            # width
3333            name = param_name+".width"
3334            param_list.append([name, param_checked, param_value, param_error,
3335                                param_npts, param_nsigs, param_min, param_max, param_fun])
3336
3337        def gatherMagnetParams(row):
3338            """
3339            Create list of magnetic parameters based on _magnet_model
3340            """
3341            param_name = str(self._magnet_model.item(row, 0).text())
3342            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3343            param_value = str(self._magnet_model.item(row, 1).text())
3344            param_error = None
3345            column_offset = 0
3346            if self.has_magnet_error_column:
3347                param_error = str(self._magnet_model.item(row, 2).text())
3348                column_offset = 1
3349            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
3350            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
3351            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3352
3353        self.iterateOverModel(gatherParams)
3354        if self.chkPolydispersity.isChecked():
3355            self.iterateOverPolyModel(gatherPolyParams)
3356        if self.chkMagnetism.isChecked() and self.chkMagnetism.isEnabled():
[b259485]3357            self.iterateOverMagnetModel(gatherMagnetParams)
[8e2cd79]3358
3359        if format=="":
3360            formatted_output = FittingUtilities.formatParameters(param_list)
3361        elif format == "Excel":
[d4ba565]3362            formatted_output = FittingUtilities.formatParametersExcel(param_list[1:])
[8e2cd79]3363        elif format == "Latex":
[d4ba565]3364            formatted_output = FittingUtilities.formatParametersLatex(param_list[1:])
[8e2cd79]3365        else:
3366            raise AttributeError("Bad format specifier.")
3367
3368        # Dump formatted_output to the clipboard
3369        cb = QtWidgets.QApplication.clipboard()
3370        cb.setText(formatted_output)
3371
3372    def onParameterPaste(self):
3373        """
3374        Use the clipboard to update fit state
3375        """
3376        # Check if the clipboard contains right stuff
3377        cb = QtWidgets.QApplication.clipboard()
3378        cb_text = cb.text()
3379
3380        context = {}
3381        # put the text into dictionary
3382        lines = cb_text.split(':')
3383        if lines[0] != 'sasview_parameter_values':
3384            return False
[0eff615]3385
3386        model = lines[1].split(',')
3387
3388        if model[0] != 'model_name':
3389            return False
3390
3391        context['model_name'] = [model[1]]
3392        for line in lines[2:-1]:
[8e2cd79]3393            if len(line) != 0:
3394                item = line.split(',')
3395                check = item[1]
3396                name = item[0]
3397                value = item[2]
3398                # Transfer the text to content[dictionary]
3399                context[name] = [check, value]
3400
3401                # limits
[685602a]3402                try:
3403                    limit_lo = item[3]
3404                    context[name].append(limit_lo)
3405                    limit_hi = item[4]
3406                    context[name].append(limit_hi)
3407                except:
3408                    pass
[8e2cd79]3409
3410                # Polydisp
3411                if len(item) > 5:
3412                    value = item[5]
3413                    context[name].append(value)
3414                    try:
3415                        value = item[6]
3416                        context[name].append(value)
3417                        value = item[7]
3418                        context[name].append(value)
3419                    except IndexError:
3420                        pass
3421
[0eff615]3422        if str(self.cbModel.currentText()) != str(context['model_name'][0]):
3423            msg = QtWidgets.QMessageBox()
3424            msg.setIcon(QtWidgets.QMessageBox.Information)
3425            msg.setText("The model in the clipboard is not the same as the currently loaded model. \
3426                         Not all parameters saved may paste correctly.")
3427            msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
3428            result = msg.exec_()
3429            if result == QtWidgets.QMessageBox.Ok:
3430                pass
3431            else:
3432                return
3433
[8e2cd79]3434        self.updateFullModel(context)
3435        self.updateFullPolyModel(context)
3436
3437    def updateFullModel(self, param_dict):
3438        """
3439        Update the model with new parameters
3440        """
3441        assert isinstance(param_dict, dict)
3442        if not dict:
3443            return
3444
3445        def updateFittedValues(row):
3446            # Utility function for main model update
3447            # internal so can use closure for param_dict
3448            param_name = str(self._model_model.item(row, 0).text())
3449            if param_name not in list(param_dict.keys()):
3450                return
3451            # checkbox state
3452            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3453            self._model_model.item(row, 0).setCheckState(param_checked)
3454
3455            # modify the param value
3456            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3457            self._model_model.item(row, 1).setText(param_repr)
3458
3459            # Potentially the error column
3460            ioffset = 0
3461            if len(param_dict[param_name])>4 and self.has_error_column:
3462                # error values are not editable - no need to update
3463                #error_repr = GuiUtils.formatNumber(param_dict[param_name][2], high=True)
3464                #self._model_model.item(row, 2).setText(error_repr)
3465                ioffset = 1
3466            # min/max
[685602a]3467            try:
3468                param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3469                self._model_model.item(row, 2+ioffset).setText(param_repr)
3470                param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3471                self._model_model.item(row, 3+ioffset).setText(param_repr)
3472            except:
3473                pass
3474
[0eff615]3475            self.setFocus()
3476
[8e2cd79]3477
[685602a]3478
[8e2cd79]3479        # block signals temporarily, so we don't end up
3480        # updating charts with every single model change on the end of fitting
3481        self._model_model.blockSignals(True)
3482        self.iterateOverModel(updateFittedValues)
3483        self._model_model.blockSignals(False)
3484
[0eff615]3485
[8e2cd79]3486    def updateFullPolyModel(self, param_dict):
3487        """
3488        Update the polydispersity model with new parameters, create the errors column
3489        """
3490        assert isinstance(param_dict, dict)
3491        if not dict:
3492            return
3493
3494        def updateFittedValues(row):
3495            # Utility function for main model update
3496            # internal so can use closure for param_dict
3497            if row >= self._poly_model.rowCount():
3498                return
3499            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
3500            if param_name not in list(param_dict.keys()):
3501                return
3502            # checkbox state
3503            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3504            self._poly_model.item(row,0).setCheckState(param_checked)
3505
3506            # modify the param value
3507            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3508            self._poly_model.item(row, 1).setText(param_repr)
3509
3510            # Potentially the error column
3511            ioffset = 0
3512            if len(param_dict[param_name])>4 and self.has_poly_error_column:
3513                ioffset = 1
3514            # min
3515            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3516            self._poly_model.item(row, 2+ioffset).setText(param_repr)
3517            # max
3518            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3519            self._poly_model.item(row, 3+ioffset).setText(param_repr)
3520            # Npts
3521            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
3522            self._poly_model.item(row, 4+ioffset).setText(param_repr)
3523            # Nsigs
3524            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3525            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3526
3527            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3528            self._poly_model.item(row, 5+ioffset).setText(param_repr)
[0eff615]3529            self.setFocus()
[8e2cd79]3530
3531        # block signals temporarily, so we don't end up
3532        # updating charts with every single model change on the end of fitting
3533        self._poly_model.blockSignals(True)
3534        self.iterateOverPolyModel(updateFittedValues)
3535        self._poly_model.blockSignals(False)
3536
[339e22b]3537
3538
Note: See TracBrowser for help on using the repository browser.