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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 33d5956 was 33d5956, checked in by ibressler, 6 years ago

FittingWidget?.updateData(): ensure theory is recalc. before plot

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