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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 80a327d was 80a327d, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

move updateRadiusEffective to class method

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