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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 9b9ec10 was 9b9ec10, checked in by Laura Forster <Awork@…>, 6 years ago

Merge branch 'ESS_GUI' of https://github.com/SasView/sasview into ESS_GUI

  • Property mode set to 100644
File size: 136.3 KB
RevLine 
[60af928]1import json
[cd31251]2import os
[60af928]3from collections import defaultdict
[b3e8629]4
[d4dac80]5import copy
[5236449]6import logging
7import traceback
[cbcdd2c]8from twisted.internet import threads
[1bc27f1]9import numpy as np
[e90988c]10import webbrowser
[5236449]11
[4992ff2]12from PyQt5 import QtCore
13from PyQt5 import QtGui
14from PyQt5 import QtWidgets
[60af928]15
16from sasmodels import generate
17from sasmodels import modelinfo
[5236449]18from sasmodels.sasview_model import load_standard_models
[18d5c94a]19from sasmodels.sasview_model import MultiplicationModel
[358b39d]20from sasmodels.weights import MODELS as POLYDISPERSITY_MODELS
21
[f182f93]22from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
[57be490]23from sas.sascalc.fit.pagestate import PageState
[5236449]24
[83eb5208]25import sas.qtgui.Utilities.GuiUtils as GuiUtils
[14ec91c5]26import sas.qtgui.Utilities.LocalConfig as LocalConfig
[dc5ef15]27from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
28from sas.qtgui.Plotting.PlotterData import Data1D
29from sas.qtgui.Plotting.PlotterData import Data2D
[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)
[3b3b40b]558
[3d18691]559        # Communicator signal
560        self.communicate.updateModelCategoriesSignal.connect(self.onCategoriesChanged)
561
[d3c0b95]562    def modelName(self):
563        """
564        Returns model name, by default M<tab#>, e.g. M1, M2
565        """
566        return "M%i" % self.tab_id
567
568    def nameForFittedData(self, name):
569        """
570        Generate name for the current fit
571        """
572        if self.is2D:
573            name += "2d"
574        name = "%s [%s]" % (self.modelName(), name)
575        return name
[7fd20fc]576
[d3c0b95]577    def showModelContextMenu(self, position):
578        """
579        Show context specific menu in the parameter table.
580        When clicked on parameter(s): fitting/constraints options
581        When clicked on white space: model description
582        """
[50cafe7]583        rows = [s.row() for s in self.lstParams.selectionModel().selectedRows()
584                if self.isCheckable(s.row())]
[7fd20fc]585        menu = self.showModelDescription() if not rows else self.modelContextMenu(rows)
586        try:
587            menu.exec_(self.lstParams.viewport().mapToGlobal(position))
588        except AttributeError as ex:
[bb477f5]589            logger.error("Error generating context menu: %s" % ex)
[7fd20fc]590        return
591
592    def modelContextMenu(self, rows):
[eae226b]593        """
[d3c0b95]594        Create context menu for the parameter selection
[eae226b]595        """
[7fd20fc]596        menu = QtWidgets.QMenu()
[eae226b]597        num_rows = len(rows)
[63319b0]598        if num_rows < 1:
599            return menu
[7fd20fc]600        # Select for fitting
[8e2cd79]601        param_string = "parameter " if num_rows == 1 else "parameters "
602        to_string = "to its current value" if num_rows == 1 else "to their current values"
[d3c0b95]603        has_constraints = any([self.rowHasConstraint(i) for i in rows])
[eae226b]604
[7fd20fc]605        self.actionSelect = QtWidgets.QAction(self)
606        self.actionSelect.setObjectName("actionSelect")
[eae226b]607        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
[7fd20fc]608        # Unselect from fitting
609        self.actionDeselect = QtWidgets.QAction(self)
610        self.actionDeselect.setObjectName("actionDeselect")
[eae226b]611        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
[7fd20fc]612
613        self.actionConstrain = QtWidgets.QAction(self)
614        self.actionConstrain.setObjectName("actionConstrain")
[eae226b]615        self.actionConstrain.setText(QtCore.QCoreApplication.translate("self", "Constrain "+param_string + to_string))
616
617        self.actionRemoveConstraint = QtWidgets.QAction(self)
618        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
619        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove constraint"))
[7fd20fc]620
621        self.actionMultiConstrain = QtWidgets.QAction(self)
622        self.actionMultiConstrain.setObjectName("actionMultiConstrain")
623        self.actionMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Constrain selected parameters to their current values"))
624
625        self.actionMutualMultiConstrain = QtWidgets.QAction(self)
626        self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
627        self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of selected parameters..."))
628
629        menu.addAction(self.actionSelect)
630        menu.addAction(self.actionDeselect)
631        menu.addSeparator()
632
[d3c0b95]633        if has_constraints:
[eae226b]634            menu.addAction(self.actionRemoveConstraint)
[d3c0b95]635            #if num_rows == 1:
636            #    menu.addAction(self.actionEditConstraint)
[eae226b]637        else:
[7fd20fc]638            menu.addAction(self.actionConstrain)
[d3c0b95]639            if num_rows == 2:
640                menu.addAction(self.actionMutualMultiConstrain)
[7fd20fc]641
642        # Define the callbacks
[0595bb7]643        self.actionConstrain.triggered.connect(self.addSimpleConstraint)
[eae226b]644        self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
[0595bb7]645        self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
[7fd20fc]646        self.actionSelect.triggered.connect(self.selectParameters)
647        self.actionDeselect.triggered.connect(self.deselectParameters)
648        return menu
649
[0595bb7]650    def showMultiConstraint(self):
[7fd20fc]651        """
652        Show the constraint widget and receive the expression
653        """
[0595bb7]654        selected_rows = self.lstParams.selectionModel().selectedRows()
[6e58f2f]655        # There have to be only two rows selected. The caller takes care of that
656        # but let's check the correctness.
[8e2cd79]657        assert len(selected_rows) == 2
[0595bb7]658
659        params_list = [s.data() for s in selected_rows]
[eae226b]660        # Create and display the widget for param1 and param2
[7fd20fc]661        mc_widget = MultiConstraint(self, params=params_list)
[305114c]662        # Check if any of the parameters are polydisperse
663        if not np.any([FittingUtilities.isParamPolydisperse(p, self.model_parameters, is2D=self.is2D) for p in params_list]):
664            # no parameters are pd - reset the text to not show the warning
665            mc_widget.lblWarning.setText("")
[eae226b]666        if mc_widget.exec_() != QtWidgets.QDialog.Accepted:
667            return
668
[0595bb7]669        constraint = Constraint()
670        c_text = mc_widget.txtConstraint.text()
671
[eae226b]672        # widget.params[0] is the parameter we're constraining
673        constraint.param = mc_widget.params[0]
[3b3b40b]674        # parameter should have the model name preamble
[06234fc]675        model_name = self.kernel_module.name
[3b3b40b]676        # param_used is the parameter we're using in constraining function
677        param_used = mc_widget.params[1]
678        # Replace param_used with model_name.param_used
679        updated_param_used = model_name + "." + param_used
680        new_func = c_text.replace(param_used, updated_param_used)
681        constraint.func = new_func
[d3c0b95]682        # Which row is the constrained parameter in?
[2d466e4]683        row = self.getRowFromName(constraint.param)
[eae226b]684
[6e58f2f]685        # Create a new item and add the Constraint object as a child
686        self.addConstraintToRow(constraint=constraint, row=row)
[eae226b]687
[2d466e4]688    def getRowFromName(self, name):
[eae226b]689        """
[d3c0b95]690        Given parameter name get the row number in self._model_model
[eae226b]691        """
692        for row in range(self._model_model.rowCount()):
693            row_name = self._model_model.item(row).text()
694            if row_name == name:
695                return row
696        return None
697
[2d466e4]698    def getParamNames(self):
699        """
700        Return list of all parameters for the current model
701        """
[00b7ddf0]702        return [self._model_model.item(row).text()
703                for row in range(self._model_model.rowCount())
704                if self.isCheckable(row)]
[2d466e4]705
[eae226b]706    def modifyViewOnRow(self, row, font=None, brush=None):
707        """
708        Chage how the given row of the main model is shown
709        """
710        fields_enabled = False
711        if font is None:
712            font = QtGui.QFont()
713            fields_enabled = True
714        if brush is None:
715            brush = QtGui.QBrush()
716            fields_enabled = True
[0595bb7]717        self._model_model.blockSignals(True)
718        # Modify font and foreground of affected rows
719        for column in range(0, self._model_model.columnCount()):
720            self._model_model.item(row, column).setForeground(brush)
721            self._model_model.item(row, column).setFont(font)
[eae226b]722            self._model_model.item(row, column).setEditable(fields_enabled)
[0595bb7]723        self._model_model.blockSignals(False)
724
[c5a2722f]725    def addConstraintToRow(self, constraint=None, row=0):
726        """
727        Adds the constraint object to requested row
728        """
729        # Create a new item and add the Constraint object as a child
[8e2cd79]730        assert isinstance(constraint, Constraint)
731        assert 0 <= row <= self._model_model.rowCount()
[00b7ddf0]732        assert self.isCheckable(row)
[c5a2722f]733
734        item = QtGui.QStandardItem()
735        item.setData(constraint)
736        self._model_model.item(row, 1).setChild(0, item)
737        # Set min/max to the value constrained
738        self.constraintAddedSignal.emit([row])
739        # Show visual hints for the constraint
740        font = QtGui.QFont()
741        font.setItalic(True)
742        brush = QtGui.QBrush(QtGui.QColor('blue'))
743        self.modifyViewOnRow(row, font=font, brush=brush)
744        self.communicate.statusBarUpdateSignal.emit('Constraint added')
745
[0595bb7]746    def addSimpleConstraint(self):
[7fd20fc]747        """
748        Adds a constraint on a single parameter.
[2add354]749        """
[d3c0b95]750        min_col = self.lstParams.itemDelegate().param_min
751        max_col = self.lstParams.itemDelegate().param_max
[0595bb7]752        for row in self.selectedParameters():
[00b7ddf0]753            assert(self.isCheckable(row))
[0595bb7]754            param = self._model_model.item(row, 0).text()
755            value = self._model_model.item(row, 1).text()
[235d766]756            min_t = self._model_model.item(row, min_col).text()
757            max_t = self._model_model.item(row, max_col).text()
[eae226b]758            # Create a Constraint object
[235d766]759            constraint = Constraint(param=param, value=value, min=min_t, max=max_t)
[eae226b]760            # Create a new item and add the Constraint object as a child
[0595bb7]761            item = QtGui.QStandardItem()
762            item.setData(constraint)
763            self._model_model.item(row, 1).setChild(0, item)
[235d766]764            # Assumed correctness from the validator
765            value = float(value)
766            # BUMPS calculates log(max-min) without any checks, so let's assign minor range
767            min_v = value - (value/10000.0)
768            max_v = value + (value/10000.0)
[eae226b]769            # Set min/max to the value constrained
[235d766]770            self._model_model.item(row, min_col).setText(str(min_v))
771            self._model_model.item(row, max_col).setText(str(max_v))
[be8f4b0]772            self.constraintAddedSignal.emit([row])
[eae226b]773            # Show visual hints for the constraint
774            font = QtGui.QFont()
775            font.setItalic(True)
776            brush = QtGui.QBrush(QtGui.QColor('blue'))
777            self.modifyViewOnRow(row, font=font, brush=brush)
[7fd20fc]778        self.communicate.statusBarUpdateSignal.emit('Constraint added')
779
[eae226b]780    def deleteConstraint(self):
781        """
782        Delete constraints from selected parameters.
783        """
[8e2cd79]784        params = [s.data() for s in self.lstParams.selectionModel().selectedRows()
[3b3b40b]785                   if self.isCheckable(s.row())]
786        for param in params:
787            self.deleteConstraintOnParameter(param=param)
[be8f4b0]788
789    def deleteConstraintOnParameter(self, param=None):
790        """
791        Delete the constraint on model parameter 'param'
792        """
[d3c0b95]793        min_col = self.lstParams.itemDelegate().param_min
794        max_col = self.lstParams.itemDelegate().param_max
[be8f4b0]795        for row in range(self._model_model.rowCount()):
[00b7ddf0]796            if not self.isCheckable(row):
797                continue
[3b3b40b]798            if not self.rowHasConstraint(row):
799                continue
[eae226b]800            # Get the Constraint object from of the model item
801            item = self._model_model.item(row, 1)
[3b3b40b]802            constraint = self.getConstraintForRow(row)
[d3c0b95]803            if constraint is None:
804                continue
805            if not isinstance(constraint, Constraint):
806                continue
[be8f4b0]807            if param and constraint.param != param:
808                continue
809            # Now we got the right row. Delete the constraint and clean up
[eae226b]810            # Retrieve old values and put them on the model
811            if constraint.min is not None:
[d3c0b95]812                self._model_model.item(row, min_col).setText(constraint.min)
[eae226b]813            if constraint.max is not None:
[d3c0b95]814                self._model_model.item(row, max_col).setText(constraint.max)
[eae226b]815            # Remove constraint item
816            item.removeRow(0)
[be8f4b0]817            self.constraintAddedSignal.emit([row])
[eae226b]818            self.modifyViewOnRow(row)
[be8f4b0]819
[eae226b]820        self.communicate.statusBarUpdateSignal.emit('Constraint removed')
[be8f4b0]821
[d3c0b95]822    def getConstraintForRow(self, row):
823        """
[c4b23dd]824        For the given row, return its constraint, if any (otherwise None)
[d3c0b95]825        """
[c4b23dd]826        if not self.isCheckable(row):
827            return None
828        item = self._model_model.item(row, 1)
829        try:
830            return item.child(0).data()
831        except AttributeError:
832            return None
[d3c0b95]833
[eae226b]834    def rowHasConstraint(self, row):
835        """
836        Finds out if row of the main model has a constraint child
837        """
[c4b23dd]838        if not self.isCheckable(row):
839            return False
840        item = self._model_model.item(row, 1)
841        if not item.hasChildren():
842            return False
843        c = item.child(0).data()
844        if isinstance(c, Constraint):
845            return True
[be8f4b0]846        return False
[116dd4c1]847
848    def rowHasActiveConstraint(self, row):
849        """
850        Finds out if row of the main model has an active constraint child
851        """
[c4b23dd]852        if not self.isCheckable(row):
853            return False
854        item = self._model_model.item(row, 1)
855        if not item.hasChildren():
856            return False
857        c = item.child(0).data()
858        if isinstance(c, Constraint) and c.active:
859            return True
[235d766]860        return False
861
862    def rowHasActiveComplexConstraint(self, row):
863        """
864        Finds out if row of the main model has an active, nontrivial constraint child
865        """
[c4b23dd]866        if not self.isCheckable(row):
867            return False
868        item = self._model_model.item(row, 1)
869        if not item.hasChildren():
870            return False
871        c = item.child(0).data()
872        if isinstance(c, Constraint) and c.func and c.active:
873            return True
[116dd4c1]874        return False
[eae226b]875
[7fd20fc]876    def selectParameters(self):
877        """
[d3c0b95]878        Selected parameter is chosen for fitting
[7fd20fc]879        """
880        status = QtCore.Qt.Checked
881        self.setParameterSelection(status)
882
883    def deselectParameters(self):
884        """
885        Selected parameters are removed for fitting
886        """
887        status = QtCore.Qt.Unchecked
888        self.setParameterSelection(status)
889
890    def selectedParameters(self):
891        """ Returns list of selected (highlighted) parameters """
[d3c0b95]892        return [s.row() for s in self.lstParams.selectionModel().selectedRows()
893                if self.isCheckable(s.row())]
[7fd20fc]894
895    def setParameterSelection(self, status=QtCore.Qt.Unchecked):
896        """
897        Selected parameters are chosen for fitting
898        """
899        # Convert to proper indices and set requested enablement
900        for row in self.selectedParameters():
901            self._model_model.item(row, 0).setCheckState(status)
[d3c0b95]902
903    def getConstraintsForModel(self):
904        """
905        Return a list of tuples. Each tuple contains constraints mapped as
906        ('constrained parameter', 'function to constrain')
907        e.g. [('sld','5*sld_solvent')]
908        """
909        param_number = self._model_model.rowCount()
910        params = [(self._model_model.item(s, 0).text(),
[c5a2722f]911                    self._model_model.item(s, 1).child(0).data().func)
[116dd4c1]912                    for s in range(param_number) if self.rowHasActiveConstraint(s)]
[d3c0b95]913        return params
[7fd20fc]914
[235d766]915    def getComplexConstraintsForModel(self):
916        """
917        Return a list of tuples. Each tuple contains constraints mapped as
918        ('constrained parameter', 'function to constrain')
[06234fc]919        e.g. [('sld','5*M2.sld_solvent')].
[235d766]920        Only for constraints with defined VALUE
921        """
922        param_number = self._model_model.rowCount()
923        params = [(self._model_model.item(s, 0).text(),
924                    self._model_model.item(s, 1).child(0).data().func)
925                    for s in range(param_number) if self.rowHasActiveComplexConstraint(s)]
926        return params
927
[ba01ad1]928    def getConstraintObjectsForModel(self):
929        """
930        Returns Constraint objects present on the whole model
931        """
932        param_number = self._model_model.rowCount()
933        constraints = [self._model_model.item(s, 1).child(0).data()
934                       for s in range(param_number) if self.rowHasConstraint(s)]
935
936        return constraints
937
[3b3b40b]938    def getConstraintsForFitting(self):
939        """
940        Return a list of constraints in format ready for use in fiting
941        """
942        # Get constraints
943        constraints = self.getComplexConstraintsForModel()
944        # See if there are any constraints across models
945        multi_constraints = [cons for cons in constraints if self.isConstraintMultimodel(cons[1])]
946
947        if multi_constraints:
948            # Let users choose what to do
949            msg = "The current fit contains constraints relying on other fit pages.\n"
950            msg += "Parameters with those constraints are:\n" +\
951                '\n'.join([cons[0] for cons in multi_constraints])
952            msg += "\n\nWould you like to remove these constraints or cancel fitting?"
953            msgbox = QtWidgets.QMessageBox(self)
954            msgbox.setIcon(QtWidgets.QMessageBox.Warning)
955            msgbox.setText(msg)
956            msgbox.setWindowTitle("Existing Constraints")
957            # custom buttons
958            button_remove = QtWidgets.QPushButton("Remove")
959            msgbox.addButton(button_remove, QtWidgets.QMessageBox.YesRole)
960            button_cancel = QtWidgets.QPushButton("Cancel")
961            msgbox.addButton(button_cancel, QtWidgets.QMessageBox.RejectRole)
962            retval = msgbox.exec_()
963            if retval == QtWidgets.QMessageBox.RejectRole:
964                # cancel fit
965                raise ValueError("Fitting cancelled")
966            else:
967                # remove constraint
968                for cons in multi_constraints:
969                    self.deleteConstraintOnParameter(param=cons[0])
970                # re-read the constraints
971                constraints = self.getComplexConstraintsForModel()
972
973        return constraints
974
[7fd20fc]975    def showModelDescription(self):
976        """
977        Creates a window with model description, when right clicked in the treeview
[2add354]978        """
979        msg = 'Model description:\n'
980        if self.kernel_module is not None:
981            if str(self.kernel_module.description).rstrip().lstrip() == '':
982                msg += "Sorry, no information is available for this model."
983            else:
984                msg += self.kernel_module.description + '\n'
985        else:
986            msg += "You must select a model to get information on this"
987
[4992ff2]988        menu = QtWidgets.QMenu()
989        label = QtWidgets.QLabel(msg)
[d6b8a1d]990        action = QtWidgets.QWidgetAction(self)
[672b8ab]991        action.setDefaultWidget(label)
992        menu.addAction(action)
[7fd20fc]993        return menu
[2add354]994
[0268aed]995    def onSelectModel(self):
[cbcdd2c]996        """
[0268aed]997        Respond to select Model from list event
[cbcdd2c]998        """
[d6b8a1d]999        model = self.cbModel.currentText()
[0268aed]1000
[f4aa7a8]1001        # Assure the control is active
1002        if not self.cbModel.isEnabled():
1003            return
1004        # Empty combobox forced to be read
[b3e8629]1005        if not model:
1006            return
[0268aed]1007
[f182f93]1008        # Reset parameters to fit
[6dbff18]1009        self.resetParametersToFit()
[d7ff531]1010        self.has_error_column = False
[aca8418]1011        self.has_poly_error_column = False
[f182f93]1012
[605d944]1013        structure = None
1014        if self.cbStructureFactor.isEnabled():
1015            structure = str(self.cbStructureFactor.currentText())
1016        self.respondToModelStructure(model=model, structure_factor=structure)
[fd1ae6d1]1017
[ee18d33]1018    def onSelectBatchFilename(self, data_index):
1019        """
1020        Update the logic based on the selected file in batch fitting
1021        """
[d4dac80]1022        self.data_index = data_index
[ee18d33]1023        self.updateQRange()
1024
[fd1ae6d1]1025    def onSelectStructureFactor(self):
1026        """
1027        Select Structure Factor from list
1028        """
1029        model = str(self.cbModel.currentText())
1030        category = str(self.cbCategory.currentText())
1031        structure = str(self.cbStructureFactor.currentText())
1032        if category == CATEGORY_STRUCTURE:
1033            model = None
[e11106e]1034
1035        # Reset parameters to fit
[6dbff18]1036        self.resetParametersToFit()
[e11106e]1037        self.has_error_column = False
1038        self.has_poly_error_column = False
1039
[fd1ae6d1]1040        self.respondToModelStructure(model=model, structure_factor=structure)
1041
[6dbff18]1042    def resetParametersToFit(self):
1043        """
1044        Clears the list of parameters to be fitted
1045        """
1046        self.main_params_to_fit = []
1047        self.poly_params_to_fit = []
1048        self.magnet_params_to_fit = []
1049
[3b3b40b]1050    def onCustomModelChange(self):
1051        """
1052        Reload the custom model combobox
1053        """
1054        self.custom_models = self.customModels()
1055        self.readCustomCategoryInfo()
[2dce1d8]1056        self.onCategoriesChanged()
1057
[3b3b40b]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]
[9d130f3]1538        if self.calc_fit is not None and self.calc_fit._interrupting:
[ded5e77]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])
[2dce1d8]1982        if plugin_list:
1983            self.master_category_dict[CATEGORY_CUSTOM] = plugin_list
[3b3b40b]1984
[5236449]1985    def regenerateModelDict(self):
[60af928]1986        """
[cbcdd2c]1987        Regenerates self.by_model_dict which has each model name as the
[60af928]1988        key and the list of categories belonging to that model
1989        along with the enabled mapping
1990        """
1991        self.by_model_dict = defaultdict(list)
1992        for category in self.master_category_dict:
1993            for (model, enabled) in self.master_category_dict[category]:
1994                self.by_model_dict[model].append(category)
1995                self.model_enabled_dict[model] = enabled
1996
[86f88d1]1997    def addBackgroundToModel(self, model):
1998        """
1999        Adds background parameter with default values to the model
2000        """
[cbcdd2c]2001        assert isinstance(model, QtGui.QStandardItemModel)
[86f88d1]2002        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
[4d457df]2003        FittingUtilities.addCheckedListToModel(model, checked_list)
[2add354]2004        last_row = model.rowCount()-1
2005        model.item(last_row, 0).setEditable(False)
2006        model.item(last_row, 4).setEditable(False)
[86f88d1]2007
2008    def addScaleToModel(self, model):
2009        """
2010        Adds scale parameter with default values to the model
2011        """
[cbcdd2c]2012        assert isinstance(model, QtGui.QStandardItemModel)
[86f88d1]2013        checked_list = ['scale', '1.0', '0.0', 'inf', '']
[4d457df]2014        FittingUtilities.addCheckedListToModel(model, checked_list)
[2add354]2015        last_row = model.rowCount()-1
2016        model.item(last_row, 0).setEditable(False)
2017        model.item(last_row, 4).setEditable(False)
[86f88d1]2018
[9d266d2]2019    def addWeightingToData(self, data):
2020        """
2021        Adds weighting contribution to fitting data
[1bc27f1]2022        """
[b764ae5]2023        new_data = copy.deepcopy(data)
[e1e3e09]2024        # Send original data for weighting
[dc5ef15]2025        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
[9a7c81c]2026        if self.is2D:
[b764ae5]2027            new_data.err_data = weight
[9a7c81c]2028        else:
[b764ae5]2029            new_data.dy = weight
2030
2031        return new_data
[9d266d2]2032
[0268aed]2033    def updateQRange(self):
2034        """
2035        Updates Q Range display
2036        """
2037        if self.data_is_loaded:
2038            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
2039        # set Q range labels on the main tab
2040        self.lblMinRangeDef.setText(str(self.q_range_min))
2041        self.lblMaxRangeDef.setText(str(self.q_range_max))
2042        # set Q range labels on the options tab
[180bd54]2043        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
[0268aed]2044
[4d457df]2045    def SASModelToQModel(self, model_name, structure_factor=None):
[60af928]2046        """
[cbcdd2c]2047        Setting model parameters into table based on selected category
[60af928]2048        """
2049        # Crete/overwrite model items
2050        self._model_model.clear()
[f3a19ad]2051        self._poly_model.clear()
2052        self._magnet_model.clear()
[5236449]2053
[f3a19ad]2054        if model_name is None:
2055            if structure_factor not in (None, "None"):
2056                # S(Q) on its own, treat the same as a form factor
2057                self.kernel_module = None
2058                self.fromStructureFactorToQModel(structure_factor)
2059            else:
2060                # No models selected
2061                return
[cd31251]2062        else:
[fd1ae6d1]2063            self.fromModelToQModel(model_name)
[70f4458]2064            self.addExtraShells()
[5236449]2065
[fd1ae6d1]2066            # Allow the SF combobox visibility for the given sasmodel
2067            self.enableStructureFactorControl(structure_factor)
[4ea8020]2068       
2069            # Add S(Q)
[605d944]2070            if self.cbStructureFactor.isEnabled():
2071                structure_factor = self.cbStructureFactor.currentText()
2072                self.fromStructureFactorToQModel(structure_factor)
[cd31251]2073
[f3a19ad]2074            # Add polydispersity to the model
[4ea8020]2075            self.poly_params = {}
[f3a19ad]2076            self.setPolyModel()
2077            # Add magnetic parameters to the model
[4ea8020]2078            self.magnet_params = {}
[f3a19ad]2079            self.setMagneticModel()
[5236449]2080
2081        # Now we claim the model has been loaded
[86f88d1]2082        self.model_is_loaded = True
[be8f4b0]2083        # Change the model name to a monicker
2084        self.kernel_module.name = self.modelName()
[9a7c81c]2085        # Update the smearing tab
2086        self.smearing_widget.updateKernelModel(kernel_model=self.kernel_module)
[86f88d1]2087
[fd1ae6d1]2088        # (Re)-create headers
2089        FittingUtilities.addHeadersToModel(self._model_model)
[6964d44]2090        self.lstParams.header().setFont(self.boldFont)
[fd1ae6d1]2091
[5236449]2092        # Update Q Ranges
2093        self.updateQRange()
2094
[fd1ae6d1]2095    def fromModelToQModel(self, model_name):
2096        """
2097        Setting model parameters into QStandardItemModel based on selected _model_
2098        """
[3b3b40b]2099        name = model_name
[e3df84e]2100        kernel_module = None
[3b3b40b]2101        if self.cbCategory.currentText() == CATEGORY_CUSTOM:
2102            # custom kernel load requires full path
2103            name = os.path.join(ModelUtilities.find_plugins_dir(), model_name+".py")
[060413c]2104        try:
2105            kernel_module = generate.load_kernel_module(name)
[e3df84e]2106        except ModuleNotFoundError as ex:
2107            pass
2108
2109        if kernel_module is None:
2110            # mismatch between "name" attribute and actual filename.
2111            curr_model = self.models[model_name]
2112            name, _ = os.path.splitext(os.path.basename(curr_model.filename))
2113            try:
2114                kernel_module = generate.load_kernel_module(name)
2115            except ModuleNotFoundError as ex:
[bb477f5]2116                logger.error("Can't find the model "+ str(ex))
[e3df84e]2117                return
[dc71408]2118
2119        if hasattr(kernel_module, 'parameters'):
2120            # built-in and custom models
2121            self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
2122
2123        elif hasattr(kernel_module, 'model_info'):
2124            # for sum/multiply models
2125            self.model_parameters = kernel_module.model_info.parameters
2126
2127        elif hasattr(kernel_module, 'Model') and hasattr(kernel_module.Model, "_model_info"):
2128            # this probably won't work if there's no model_info, but just in case
2129            self.model_parameters = kernel_module.Model._model_info.parameters
2130        else:
2131            # no parameters - default to blank table
2132            msg = "No parameters found in model '{}'.".format(model_name)
2133            logger.warning(msg)
2134            self.model_parameters = modelinfo.ParameterTable([])
[fd1ae6d1]2135
2136        # Instantiate the current sasmodel
2137        self.kernel_module = self.models[model_name]()
2138
[9ce69ec]2139        # Change the model name to a monicker
2140        self.kernel_module.name = self.modelName()
2141
[fd1ae6d1]2142        # Explicitly add scale and background with default values
[6964d44]2143        temp_undo_state = self.undo_supported
2144        self.undo_supported = False
[fd1ae6d1]2145        self.addScaleToModel(self._model_model)
2146        self.addBackgroundToModel(self._model_model)
[6964d44]2147        self.undo_supported = temp_undo_state
[fd1ae6d1]2148
[0d13814]2149        self.shell_names = self.shellNamesList()
2150
[00b7ddf0]2151        # Add heading row
2152        FittingUtilities.addHeadingRowToModel(self._model_model, model_name)
[aca8418]2153
[fd1ae6d1]2154        # Update the QModel
[04f775d]2155        FittingUtilities.addParametersToModel(
2156                self.model_parameters,
2157                self.kernel_module,
[a758043]2158                self.is2D,
2159                self._model_model,
2160                self.lstParams)
[fd1ae6d1]2161
2162    def fromStructureFactorToQModel(self, structure_factor):
2163        """
2164        Setting model parameters into QStandardItemModel based on selected _structure factor_
2165        """
[605d944]2166        if structure_factor is None or structure_factor=="None":
2167            return
[18d5c94a]2168
[01b4877]2169        product_params = None
2170
[5fb714b]2171        if self.kernel_module is None:
2172            # Structure factor is the only selected model; build it and show all its params
2173            self.kernel_module = self.models[structure_factor]()
[9ce69ec]2174            self.kernel_module.name = self.modelName()
[5fb714b]2175            s_params = self.kernel_module._model_info.parameters
2176            s_params_orig = s_params
[f3a19ad]2177        else:
[5fb714b]2178            s_kernel = self.models[structure_factor]()
2179            p_kernel = self.kernel_module
[5d1440e1]2180
[f3a19ad]2181            p_pars_len = len(p_kernel._model_info.parameters.kernel_parameters)
2182            s_pars_len = len(s_kernel._model_info.parameters.kernel_parameters)
[fd1ae6d1]2183
[f3a19ad]2184            self.kernel_module = MultiplicationModel(p_kernel, s_kernel)
[9ce69ec]2185            # Modify the name to correspond to shown items
2186            self.kernel_module.name = self.modelName()
[f3a19ad]2187            all_params = self.kernel_module._model_info.parameters.kernel_parameters
2188            all_param_names = [param.name for param in all_params]
[18d5c94a]2189
[f3a19ad]2190            # S(Q) params from the product model are not necessarily the same as those from the S(Q) model; any
[5fb714b]2191            # conflicting names with P(Q) params will cause a rename
[fd1ae6d1]2192
[f3a19ad]2193            if "radius_effective_mode" in all_param_names:
2194                # Show all parameters
[01b4877]2195                # In this case, radius_effective is NOT pruned by sasmodels.product
[f3a19ad]2196                s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len])
2197                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters)
[01b4877]2198                product_params = modelinfo.ParameterTable(
2199                        self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
[f3a19ad]2200            else:
2201                # Ensure radius_effective is not displayed
2202                s_params_orig = modelinfo.ParameterTable(s_kernel._model_info.parameters.kernel_parameters[1:])
2203                if "radius_effective" in all_param_names:
[01b4877]2204                    # In this case, radius_effective is NOT pruned by sasmodels.product
[f3a19ad]2205                    s_params = modelinfo.ParameterTable(all_params[p_pars_len+1:p_pars_len+s_pars_len])
[01b4877]2206                    product_params = modelinfo.ParameterTable(
2207                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len:])
[f3a19ad]2208                else:
[01b4877]2209                    # In this case, radius_effective is pruned by sasmodels.product
[f3a19ad]2210                    s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len-1])
[01b4877]2211                    product_params = modelinfo.ParameterTable(
2212                            self.kernel_module._model_info.parameters.kernel_parameters[p_pars_len+s_pars_len-1:])
[f3a19ad]2213
[00b7ddf0]2214        # Add heading row
2215        FittingUtilities.addHeadingRowToModel(self._model_model, structure_factor)
[0d72cac]2216
[b69b549]2217        # Get new rows for QModel
2218        # Any renamed parameters are stored as data in the relevant item, for later handling
[04f775d]2219        FittingUtilities.addSimpleParametersToModel(
[01b4877]2220                parameters=s_params,
2221                is2D=self.is2D,
2222                parameters_original=s_params_orig,
2223                model=self._model_model,
2224                view=self.lstParams)
2225
2226        # Insert product-only params into QModel
2227        if product_params:
2228            prod_rows = FittingUtilities.addSimpleParametersToModel(
2229                    parameters=product_params,
2230                    is2D=self.is2D,
2231                    parameters_original=None,
2232                    model=self._model_model,
2233                    view=self.lstParams,
2234                    row_num=2)
2235
2236            # Since this all happens after shells are dealt with and we've inserted rows, fix this counter
2237            self._n_shells_row += len(prod_rows)
[fd1ae6d1]2238
[6dbff18]2239    def haveParamsToFit(self):
2240        """
2241        Finds out if there are any parameters ready to be fitted
2242        """
2243        return (self.main_params_to_fit!=[]
2244                or self.poly_params_to_fit!=[]
2245                or self.magnet_params_to_fit != []) and \
2246                self.logic.data_is_loaded
2247
[b00414d]2248    def onMainParamsChange(self, item):
[cd31251]2249        """
2250        Callback method for updating the sasmodel parameters with the GUI values
2251        """
[cbcdd2c]2252        model_column = item.column()
[cd31251]2253
2254        if model_column == 0:
[f182f93]2255            self.checkboxSelected(item)
[6dbff18]2256            self.cmdFit.setEnabled(self.haveParamsToFit())
[6964d44]2257            # Update state stack
2258            self.updateUndo()
[cd31251]2259            return
2260
[f182f93]2261        model_row = item.row()
2262        name_index = self._model_model.index(model_row, 0)
[700b03b]2263        name_item = self._model_model.itemFromIndex(name_index)
[f182f93]2264
[b00414d]2265        # Extract changed value.
[2add354]2266        try:
[fbfc488]2267            value = GuiUtils.toDouble(item.text())
[0261bc1]2268        except TypeError:
[2add354]2269            # Unparsable field
2270            return
[fbfc488]2271
[700b03b]2272        # if the item has user data, this is the actual parameter name (e.g. to handle duplicate names)
2273        if name_item.data(QtCore.Qt.UserRole):
2274            parameter_name = str(name_item.data(QtCore.Qt.UserRole))
2275        else:
2276            parameter_name = str(self._model_model.data(name_index))
[cbcdd2c]2277
[00b3b40]2278        # Update the parameter value - note: this supports +/-inf as well
[cbcdd2c]2279        self.kernel_module.params[parameter_name] = value
2280
[8a32a6ff]2281        # Update the parameter value - note: this supports +/-inf as well
[8f2548c]2282        param_column = self.lstParams.itemDelegate().param_value
2283        min_column = self.lstParams.itemDelegate().param_min
2284        max_column = self.lstParams.itemDelegate().param_max
2285        if model_column == param_column:
[8a32a6ff]2286            self.kernel_module.setParam(parameter_name, value)
[8f2548c]2287        elif model_column == min_column:
[8a32a6ff]2288            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
[8f2548c]2289            self.kernel_module.details[parameter_name][1] = value
2290        elif model_column == max_column:
2291            self.kernel_module.details[parameter_name][2] = value
2292        else:
2293            # don't update the chart
2294            return
[00b3b40]2295
2296        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
2297        # TODO: multishell params in self.kernel_module.details[??] = value
[cbcdd2c]2298
[d7ff531]2299        # Force the chart update when actual parameters changed
2300        if model_column == 1:
[d48cc19]2301            self.recalculatePlotData()
[7d077d1]2302
[2241130]2303        # Update state stack
[00b3b40]2304        self.updateUndo()
[2241130]2305
[7fd20fc]2306    def isCheckable(self, row):
2307        return self._model_model.item(row, 0).isCheckable()
2308
[f182f93]2309    def checkboxSelected(self, item):
2310        # Assure we're dealing with checkboxes
2311        if not item.isCheckable():
2312            return
2313        status = item.checkState()
2314
2315        # If multiple rows selected - toggle all of them, filtering uncheckable
2316        # Switch off signaling from the model to avoid recursion
2317        self._model_model.blockSignals(True)
2318        # Convert to proper indices and set requested enablement
[7fd20fc]2319        self.setParameterSelection(status)
[f182f93]2320        self._model_model.blockSignals(False)
2321
2322        # update the list of parameters to fit
[6dbff18]2323        self.main_params_to_fit = self.checkedListFromModel(self._model_model)
[c1e380e]2324
2325    def checkedListFromModel(self, model):
2326        """
2327        Returns list of checked parameters for given model
2328        """
2329        def isChecked(row):
2330            return model.item(row, 0).checkState() == QtCore.Qt.Checked
2331
2332        return [str(model.item(row_index, 0).text())
[b3e8629]2333                for row_index in range(model.rowCount())
[c1e380e]2334                if isChecked(row_index)]
[f182f93]2335
[6fd4e36]2336    def createNewIndex(self, fitted_data):
2337        """
2338        Create a model or theory index with passed Data1D/Data2D
2339        """
2340        if self.data_is_loaded:
[0268aed]2341            if not fitted_data.name:
2342                name = self.nameForFittedData(self.data.filename)
2343                fitted_data.title = name
2344                fitted_data.name = name
2345                fitted_data.filename = name
[7d077d1]2346                fitted_data.symbol = "Line"
[6fd4e36]2347            self.updateModelIndex(fitted_data)
2348        else:
[3ae9179]2349            if not fitted_data.name:
2350                name = self.nameForFittedData(self.kernel_module.id)
2351            else:
2352                name = fitted_data.name
[0268aed]2353            fitted_data.title = name
2354            fitted_data.filename = name
2355            fitted_data.symbol = "Line"
[6fd4e36]2356            self.createTheoryIndex(fitted_data)
[5d28d6b]2357            # Switch to the theory tab for user's glee
2358            self.communicate.changeDataExplorerTabSignal.emit(1)
[6fd4e36]2359
2360    def updateModelIndex(self, fitted_data):
2361        """
2362        Update a QStandardModelIndex containing model data
2363        """
[00b3b40]2364        name = self.nameFromData(fitted_data)
[0268aed]2365        # Make this a line if no other defined
[7d077d1]2366        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
[0268aed]2367            fitted_data.symbol = 'Line'
[6fd4e36]2368        # Notify the GUI manager so it can update the main model in DataExplorer
[d4dac80]2369        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
[6fd4e36]2370
2371    def createTheoryIndex(self, fitted_data):
2372        """
2373        Create a QStandardModelIndex containing model data
2374        """
[00b3b40]2375        name = self.nameFromData(fitted_data)
2376        # Notify the GUI manager so it can create the theory model in DataExplorer
[cb90b65]2377        self.theory_item = GuiUtils.createModelItemWithPlot(fitted_data, name=name)
2378        self.communicate.updateTheoryFromPerspectiveSignal.emit(self.theory_item)
[00b3b40]2379
2380    def nameFromData(self, fitted_data):
2381        """
2382        Return name for the dataset. Terribly impure function.
2383        """
[0268aed]2384        if fitted_data.name is None:
[00b3b40]2385            name = self.nameForFittedData(self.logic.data.filename)
[0268aed]2386            fitted_data.title = name
2387            fitted_data.name = name
2388            fitted_data.filename = name
2389        else:
2390            name = fitted_data.name
[00b3b40]2391        return name
[5236449]2392
[4d457df]2393    def methodCalculateForData(self):
2394        '''return the method for data calculation'''
2395        return Calc1D if isinstance(self.data, Data1D) else Calc2D
2396
2397    def methodCompleteForData(self):
2398        '''return the method for result parsin on calc complete '''
[d4dac80]2399        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
[4d457df]2400
[66d4370]2401    def updateKernelModelWithExtraParams(self, model=None):
2402        """
2403        Updates kernel model 'model' with extra parameters from
2404        the polydisp and magnetism tab, if the tabs are enabled
2405        """
2406        if model is None: return
2407        if not hasattr(model, 'setParam'): return
2408
2409        # add polydisperse parameters if asked
[2d47985]2410        if self.chkPolydispersity.isChecked() and self._poly_model.rowCount() > 0:
[66d4370]2411            for key, value in self.poly_params.items():
2412                model.setParam(key, value)
2413        # add magnetic params if asked
[91f4884]2414        if self.chkMagnetism.isChecked() and self._magnet_model.rowCount() > 0:
2415            for key, value in self.magnet_params.items():
[66d4370]2416                model.setParam(key, value)
2417
[d4dac80]2418    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
[86f88d1]2419        """
[d4dac80]2420        Wrapper for Calc1D/2D calls
[86f88d1]2421        """
[d4dac80]2422        if data is None:
2423            data = self.data
2424        if model is None:
[66d4370]2425            model = copy.deepcopy(self.kernel_module)
2426            self.updateKernelModelWithExtraParams(model)
2427
[d4dac80]2428        if completefn is None:
2429            completefn = self.methodCompleteForData()
[9a7c81c]2430        smearer = self.smearing_widget.smearer()
[b764ae5]2431        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
2432
[557fc498]2433        # Disable buttons/table
2434        self.disableInteractiveElements()
[4d457df]2435        # Awful API to a backend method.
[d4dac80]2436        calc_thread = self.methodCalculateForData()(data=data,
2437                                               model=model,
[1bc27f1]2438                                               page_id=0,
2439                                               qmin=self.q_range_min,
2440                                               qmax=self.q_range_max,
[9a7c81c]2441                                               smearer=smearer,
[1bc27f1]2442                                               state=None,
[b764ae5]2443                                               weight=weight,
[1bc27f1]2444                                               fid=None,
2445                                               toggle_mode_on=False,
[d4dac80]2446                                               completefn=completefn,
[1bc27f1]2447                                               update_chisqr=True,
2448                                               exception_handler=self.calcException,
2449                                               source=None)
[d4dac80]2450        if use_threads:
2451            if LocalConfig.USING_TWISTED:
2452                # start the thread with twisted
2453                thread = threads.deferToThread(calc_thread.compute)
2454                thread.addCallback(completefn)
2455                thread.addErrback(self.calculateDataFailed)
2456            else:
2457                # Use the old python threads + Queue
2458                calc_thread.queue()
2459                calc_thread.ready(2.5)
2460        else:
2461            results = calc_thread.compute()
2462            completefn(results)
[4d457df]2463
[d4dac80]2464    def calculateQGridForModel(self):
2465        """
2466        Prepare the fitting data object, based on current ModelModel
2467        """
2468        if self.kernel_module is None:
2469            return
2470        self.calculateQGridForModelExt()
[6964d44]2471
[aca8418]2472    def calculateDataFailed(self, reason):
[6964d44]2473        """
[c1e380e]2474        Thread returned error
[6964d44]2475        """
[557fc498]2476        # Bring the GUI to normal state
2477        self.enableInteractiveElements()
[b3e8629]2478        print("Calculate Data failed with ", reason)
[5236449]2479
[d4dac80]2480    def completed1D(self, return_data):
2481        self.Calc1DFinishedSignal.emit(return_data)
2482
2483    def completed2D(self, return_data):
2484        self.Calc2DFinishedSignal.emit(return_data)
2485
[cbcdd2c]2486    def complete1D(self, return_data):
[5236449]2487        """
[4d457df]2488        Plot the current 1D data
2489        """
[557fc498]2490        # Bring the GUI to normal state
2491        self.enableInteractiveElements()
[9ce69ec]2492        if return_data is None:
2493            return
[d48cc19]2494        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
[d1e4689]2495
[712db9e]2496        # assure the current index is set properly for batch
2497        if len(self._logic) > 1:
2498            for i, logic in enumerate(self._logic):
2499                if logic.data.name in fitted_data.name:
2500                    self.data_index = i
2501
[0fe7e5b]2502        residuals = self.calculateResiduals(fitted_data)
[d48cc19]2503        self.model_data = fitted_data
[5aad7a5]2504        new_plots = [fitted_data]
2505        if residuals is not None:
2506            new_plots.append(residuals)
[cbcdd2c]2507
[fd7ef36]2508        if self.data_is_loaded:
[3ae70f9]2509            # delete any plots associated with the data that were not updated
2510            # (e.g. to remove beta(Q), S_eff(Q))
[0dcb71d]2511            GuiUtils.deleteRedundantPlots(self.all_data[self.data_index], new_plots)
[5b144c6]2512            pass
[fd7ef36]2513        else:
[3ae70f9]2514            # delete theory items for the model, in order to get rid of any
2515            # redundant items, e.g. beta(Q), S_eff(Q)
[fd7ef36]2516            self.communicate.deleteIntermediateTheoryPlotsSignal.emit(self.kernel_module.id)
[0fe7e5b]2517
[d8d81ea]2518        # Create plots for parameters with enabled polydispersity
2519        for plot in FittingUtilities.plotPolydispersities(return_data.get('model', None)):
2520            data_id = fitted_data.id.split()
2521            plot.id = "{} [{}] {}".format(data_id[0], plot.name, " ".join(data_id[1:]))
2522            data_name = fitted_data.name.split()
2523            plot.name = " ".join([data_name[0], plot.name] + data_name[1:])
2524            self.createNewIndex(plot)
2525            new_plots.append(plot)
2526
[3ae9179]2527        # Create plots for intermediate product data
[40975f8]2528        plots = self.logic.new1DProductPlots(return_data, self.tab_id)
2529        for plot in plots:
2530            plot.symbol = "Line"
2531            self.createNewIndex(plot)
2532            new_plots.append(plot)
[0fe7e5b]2533
[9463ca2]2534        for plot in new_plots:
[5aad7a5]2535            self.communicate.plotUpdateSignal.emit([plot])
[9463ca2]2536
[cbcdd2c]2537    def complete2D(self, return_data):
2538        """
[4d457df]2539        Plot the current 2D data
2540        """
[557fc498]2541        # Bring the GUI to normal state
2542        self.enableInteractiveElements()
2543
[8a09457]2544        if return_data is None:
2545            return
2546
[6fd4e36]2547        fitted_data = self.logic.new2DPlot(return_data)
[8a09457]2548        # assure the current index is set properly for batch
2549        if len(self._logic) > 1:
2550            for i, logic in enumerate(self._logic):
2551                if logic.data.name in fitted_data.name:
2552                    self.data_index = i
2553
[66d4370]2554        residuals = self.calculateResiduals(fitted_data)
[d48cc19]2555        self.model_data = fitted_data
[66d4370]2556        new_plots = [fitted_data]
2557        if residuals is not None:
2558            new_plots.append(residuals)
2559
2560        # Update/generate plots
2561        for plot in new_plots:
2562            self.communicate.plotUpdateSignal.emit([plot])
[6fd4e36]2563
2564    def calculateResiduals(self, fitted_data):
2565        """
[9463ca2]2566        Calculate and print Chi2 and display chart of residuals. Returns residuals plot object.
[6fd4e36]2567        """
2568        # Create a new index for holding data
[7d077d1]2569        fitted_data.symbol = "Line"
[6964d44]2570
2571        # Modify fitted_data with weighting
[b764ae5]2572        weighted_data = self.addWeightingToData(fitted_data)
[6964d44]2573
[b764ae5]2574        self.createNewIndex(weighted_data)
[712db9e]2575
[6fd4e36]2576        # Calculate difference between return_data and logic.data
[712db9e]2577        self.chi2 = FittingUtilities.calculateChi2(weighted_data, self.data)
[6fd4e36]2578        # Update the control
[2add354]2579        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
[f182f93]2580        self.lblChi2Value.setText(chi2_repr)
[cbcdd2c]2581
[0268aed]2582        # Plot residuals if actual data
[aca8418]2583        if not self.data_is_loaded:
2584            return
2585
[b764ae5]2586        residuals_plot = FittingUtilities.plotResiduals(self.data, weighted_data)
[712db9e]2587        if residuals_plot is None:
2588            return
[aca8418]2589        residuals_plot.id = "Residual " + residuals_plot.id
[a54bbf2b]2590        residuals_plot.plot_role = Data1D.ROLE_RESIDUAL
[aca8418]2591        self.createNewIndex(residuals_plot)
[0fe7e5b]2592        return residuals_plot
[5236449]2593
[3d18691]2594    def onCategoriesChanged(self):
2595            """
2596            Reload the category/model comboboxes
2597            """
2598            # Store the current combo indices
2599            current_cat = self.cbCategory.currentText()
2600            current_model = self.cbModel.currentText()
2601
2602            # reread the category file and repopulate the combo
2603            self.cbCategory.blockSignals(True)
2604            self.cbCategory.clear()
2605            self.readCategoryInfo()
2606            self.initializeCategoryCombo()
2607
2608            # Scroll back to the original index in Categories
2609            new_index = self.cbCategory.findText(current_cat)
2610            if new_index != -1:
2611                self.cbCategory.setCurrentIndex(new_index)
2612            self.cbCategory.blockSignals(False)
2613            # ...and in the Models
2614            self.cbModel.blockSignals(True)
2615            new_index = self.cbModel.findText(current_model)
2616            if new_index != -1:
2617                self.cbModel.setCurrentIndex(new_index)
2618            self.cbModel.blockSignals(False)
2619
2620            return
2621
[5236449]2622    def calcException(self, etype, value, tb):
2623        """
[c1e380e]2624        Thread threw an exception.
[5236449]2625        """
[557fc498]2626        # Bring the GUI to normal state
2627        self.enableInteractiveElements()
[c1e380e]2628        # TODO: remimplement thread cancellation
[bb477f5]2629        logger.error("".join(traceback.format_exception(etype, value, tb)))
[60af928]2630
2631    def setTableProperties(self, table):
2632        """
2633        Setting table properties
2634        """
2635        # Table properties
2636        table.verticalHeader().setVisible(False)
2637        table.setAlternatingRowColors(True)
[4992ff2]2638        table.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
2639        table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
[f46f6dc]2640        table.resizeColumnsToContents()
2641
[60af928]2642        # Header
2643        header = table.horizontalHeader()
[4992ff2]2644        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
2645        header.ResizeMode(QtWidgets.QHeaderView.Interactive)
[f46f6dc]2646
[4992ff2]2647        # Qt5: the following 2 lines crash - figure out why!
[e43fc91]2648        # Resize column 0 and 7 to content
[4992ff2]2649        #header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
2650        #header.setSectionResizeMode(7, QtWidgets.QHeaderView.ResizeToContents)
[60af928]2651
2652    def setPolyModel(self):
2653        """
2654        Set polydispersity values
2655        """
[86f88d1]2656        if not self.model_parameters:
2657            return
2658        self._poly_model.clear()
2659
[f53bc04]2660        parameters = self.model_parameters.form_volume_parameters
2661        if self.is2D:
2662            parameters += self.model_parameters.orientation_parameters
2663
[e43fc91]2664        [self.setPolyModelParameters(i, param) for i, param in \
[f53bc04]2665            enumerate(parameters) if param.polydisperse]
2666
[4d457df]2667        FittingUtilities.addPolyHeadersToModel(self._poly_model)
[60af928]2668
[e43fc91]2669    def setPolyModelParameters(self, i, param):
[aca8418]2670        """
[0d13814]2671        Standard of multishell poly parameter driver
[aca8418]2672        """
[0d13814]2673        param_name = param.name
2674        # see it the parameter is multishell
[06b0138]2675        if '[' in param.name:
[0d13814]2676            # Skip empty shells
2677            if self.current_shell_displayed == 0:
2678                return
2679            else:
2680                # Create as many entries as current shells
[b3e8629]2681                for ishell in range(1, self.current_shell_displayed+1):
[0d13814]2682                    # Remove [n] and add the shell numeral
2683                    name = param_name[0:param_name.index('[')] + str(ishell)
[e43fc91]2684                    self.addNameToPolyModel(i, name)
[0d13814]2685        else:
2686            # Just create a simple param entry
[e43fc91]2687            self.addNameToPolyModel(i, param_name)
[0d13814]2688
[e43fc91]2689    def addNameToPolyModel(self, i, param_name):
[0d13814]2690        """
2691        Creates a checked row in the poly model with param_name
2692        """
[144ec831]2693        # Polydisp. values from the sasmodel
[0d13814]2694        width = self.kernel_module.getParam(param_name + '.width')
2695        npts = self.kernel_module.getParam(param_name + '.npts')
2696        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
2697        _, min, max = self.kernel_module.details[param_name]
[144ec831]2698
[66d4370]2699        # Update local param dict
2700        self.poly_params[param_name + '.width'] = width
2701        self.poly_params[param_name + '.npts'] = npts
2702        self.poly_params[param_name + '.nsigmas'] = nsigs
2703
[144ec831]2704        # Construct a row with polydisp. related variable.
2705        # This will get added to the polydisp. model
2706        # Note: last argument needs extra space padding for decent display of the control
[0d13814]2707        checked_list = ["Distribution of " + param_name, str(width),
2708                        str(min), str(max),
[e43fc91]2709                        str(npts), str(nsigs), "gaussian      ",'']
[aca8418]2710        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
2711
2712        # All possible polydisp. functions as strings in combobox
[4992ff2]2713        func = QtWidgets.QComboBox()
[b3e8629]2714        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.keys()])
[e43fc91]2715        # Set the default index
[aca8418]2716        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
[e43fc91]2717        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
2718        self.lstPoly.setIndexWidget(ind, func)
2719        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
2720
2721    def onPolyFilenameChange(self, row_index):
2722        """
2723        Respond to filename_updated signal from the delegate
2724        """
2725        # For the given row, invoke the "array" combo handler
2726        array_caption = 'array'
[8222f171]2727
[e43fc91]2728        # Get the combo box reference
2729        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2730        widget = self.lstPoly.indexWidget(ind)
[8222f171]2731
[e43fc91]2732        # Update the combo box so it displays "array"
2733        widget.blockSignals(True)
2734        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
2735        widget.blockSignals(False)
[aca8418]2736
[8222f171]2737        # Invoke the file reader
2738        self.onPolyComboIndexChange(array_caption, row_index)
2739
[aca8418]2740    def onPolyComboIndexChange(self, combo_string, row_index):
2741        """
2742        Modify polydisp. defaults on function choice
2743        """
[144ec831]2744        # Get npts/nsigs for current selection
[aca8418]2745        param = self.model_parameters.form_volume_parameters[row_index]
[e43fc91]2746        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
2747        combo_box = self.lstPoly.indexWidget(file_index)
[aca8418]2748
[919d47c]2749        def updateFunctionCaption(row):
2750            # Utility function for update of polydispersity function name in the main model
[00b7ddf0]2751            if not self.isCheckable(row):
2752                return
[73665a8]2753            self._model_model.blockSignals(True)
[1643d8ed]2754            param_name = str(self._model_model.item(row, 0).text())
[73665a8]2755            self._model_model.blockSignals(False)
[919d47c]2756            if param_name !=  param.name:
2757                return
[144ec831]2758            # Modify the param value
[73665a8]2759            self._model_model.blockSignals(True)
[906e0c7]2760            if self.has_error_column:
2761                # err column changes the indexing
2762                self._model_model.item(row, 0).child(0).child(0,5).setText(combo_string)
2763            else:
2764                self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
[73665a8]2765            self._model_model.blockSignals(False)
[919d47c]2766
[aca8418]2767        if combo_string == 'array':
2768            try:
[e43fc91]2769                self.loadPolydispArray(row_index)
[919d47c]2770                # Update main model for display
2771                self.iterateOverModel(updateFunctionCaption)
[e43fc91]2772                # disable the row
2773                lo = self.lstPoly.itemDelegate().poly_pd
2774                hi = self.lstPoly.itemDelegate().poly_function
[b3e8629]2775                [self._poly_model.item(row_index, i).setEnabled(False) for i in range(lo, hi)]
[aca8418]2776                return
[e43fc91]2777            except IOError:
[8222f171]2778                combo_box.setCurrentIndex(self.orig_poly_index)
[e43fc91]2779                # Pass for cancel/bad read
2780                pass
[aca8418]2781
2782        # Enable the row in case it was disabled by Array
[919d47c]2783        self._poly_model.blockSignals(True)
[e43fc91]2784        max_range = self.lstPoly.itemDelegate().poly_filename
[b3e8629]2785        [self._poly_model.item(row_index, i).setEnabled(True) for i in range(7)]
[e43fc91]2786        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
[b3e8629]2787        self._poly_model.setData(file_index, "")
[919d47c]2788        self._poly_model.blockSignals(False)
[aca8418]2789
[8eaa101]2790        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
2791        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
[aca8418]2792
2793        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
2794        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
2795
[b3e8629]2796        self._poly_model.setData(npts_index, npts)
2797        self._poly_model.setData(nsigs_index, nsigs)
[aca8418]2798
[919d47c]2799        self.iterateOverModel(updateFunctionCaption)
[8222f171]2800        self.orig_poly_index = combo_box.currentIndex()
[919d47c]2801
[e43fc91]2802    def loadPolydispArray(self, row_index):
[aca8418]2803        """
2804        Show the load file dialog and loads requested data into state
2805        """
[4992ff2]2806        datafile = QtWidgets.QFileDialog.getOpenFileName(
2807            self, "Choose a weight file", "", "All files (*.*)", None,
[fbfc488]2808            QtWidgets.QFileDialog.DontUseNativeDialog)[0]
[72f4834]2809
[fbfc488]2810        if not datafile:
[bb477f5]2811            logger.info("No weight data chosen.")
[1643d8ed]2812            raise IOError
[72f4834]2813
[aca8418]2814        values = []
2815        weights = []
[919d47c]2816        def appendData(data_tuple):
2817            """
2818            Fish out floats from a tuple of strings
2819            """
2820            try:
2821                values.append(float(data_tuple[0]))
2822                weights.append(float(data_tuple[1]))
2823            except (ValueError, IndexError):
2824                # just pass through if line with bad data
2825                return
2826
[aca8418]2827        with open(datafile, 'r') as column_file:
2828            column_data = [line.rstrip().split() for line in column_file.readlines()]
[919d47c]2829            [appendData(line) for line in column_data]
[aca8418]2830
[1643d8ed]2831        # If everything went well - update the sasmodel values
[aca8418]2832        self.disp_model = POLYDISPERSITY_MODELS['array']()
2833        self.disp_model.set_weights(np.array(values), np.array(weights))
[e43fc91]2834        # + update the cell with filename
2835        fname = os.path.basename(str(datafile))
2836        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
[b3e8629]2837        self._poly_model.setData(fname_index, fname)
[aca8418]2838
[f37cab0]2839    def onColumnWidthUpdate(self, index, old_size, new_size):
2840        """
2841        Simple state update of the current column widths in the  param list
2842        """
2843        self.lstParamHeaderSizes[index] = new_size
2844
[60af928]2845    def setMagneticModel(self):
2846        """
2847        Set magnetism values on model
2848        """
[86f88d1]2849        if not self.model_parameters:
2850            return
2851        self._magnet_model.clear()
[aca8418]2852        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
[06b0138]2853            self.model_parameters.call_parameters if param.type == 'magnetic']
[4d457df]2854        FittingUtilities.addHeadersToModel(self._magnet_model)
[60af928]2855
[0d13814]2856    def shellNamesList(self):
2857        """
2858        Returns list of names of all multi-shell parameters
2859        E.g. for sld[n], radius[n], n=1..3 it will return
2860        [sld1, sld2, sld3, radius1, radius2, radius3]
2861        """
2862        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
2863        top_index = self.kernel_module.multiplicity_info.number
2864        shell_names = []
[b3e8629]2865        for i in range(1, top_index+1):
[0d13814]2866            for name in multi_names:
2867                shell_names.append(name+str(i))
2868        return shell_names
2869
[aca8418]2870    def addCheckedMagneticListToModel(self, param, model):
2871        """
2872        Wrapper for model update with a subset of magnetic parameters
2873        """
[0d13814]2874        if param.name[param.name.index(':')+1:] in self.shell_names:
2875            # check if two-digit shell number
2876            try:
2877                shell_index = int(param.name[-2:])
2878            except ValueError:
2879                shell_index = int(param.name[-1:])
2880
2881            if shell_index > self.current_shell_displayed:
2882                return
2883
[aca8418]2884        checked_list = [param.name,
2885                        str(param.default),
2886                        str(param.limits[0]),
2887                        str(param.limits[1]),
2888                        param.units]
2889
[66d4370]2890        self.magnet_params[param.name] = param.default
2891
[aca8418]2892        FittingUtilities.addCheckedListToModel(model, checked_list)
2893
[fd1ae6d1]2894    def enableStructureFactorControl(self, structure_factor):
[cd31251]2895        """
2896        Add structure factors to the list of parameters
2897        """
[fd1ae6d1]2898        if self.kernel_module.is_form_factor or structure_factor == 'None':
[cd31251]2899            self.enableStructureCombo()
2900        else:
2901            self.disableStructureCombo()
2902
[60af928]2903    def addExtraShells(self):
2904        """
[f46f6dc]2905        Add a combobox for multiple shell display
[60af928]2906        """
[4d457df]2907        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
[f46f6dc]2908
2909        if param_length == 0:
2910            return
2911
[6f7f652]2912        # cell 1: variable name
[f46f6dc]2913        item1 = QtGui.QStandardItem(param_name)
2914
[4992ff2]2915        func = QtWidgets.QComboBox()
[60af928]2916
[6f7f652]2917        # cell 2: combobox
[f46f6dc]2918        item2 = QtGui.QStandardItem()
[13dd7d2]2919
2920        # cell 3: min value
2921        item3 = QtGui.QStandardItem()
2922
2923        # cell 4: max value
2924        item4 = QtGui.QStandardItem()
2925
[86d3207]2926        # cell 4: SLD button
2927        item5 = QtGui.QStandardItem()
2928        button = QtWidgets.QPushButton()
2929        button.setText("Show SLD Profile")
2930
2931        self._model_model.appendRow([item1, item2, item3, item4, item5])
[60af928]2932
[6f7f652]2933        # Beautify the row:  span columns 2-4
[60af928]2934        shell_row = self._model_model.rowCount()
[f46f6dc]2935        shell_index = self._model_model.index(shell_row-1, 1)
[86d3207]2936        button_index = self._model_model.index(shell_row-1, 4)
[86f88d1]2937
[4d457df]2938        self.lstParams.setIndexWidget(shell_index, func)
[86d3207]2939        self.lstParams.setIndexWidget(button_index, button)
[70f4458]2940        self._n_shells_row = shell_row - 1
[86f88d1]2941
[f712bf30]2942        # Get the default number of shells for the model
2943        kernel_pars = self.kernel_module._model_info.parameters.kernel_parameters
2944        shell_par = None
2945        for par in kernel_pars:
2946            if par.name == param_name:
2947                shell_par = par
2948                break
2949        if not shell_par:
2950            logger.error("Could not find %s in kernel parameters.", param_name)
2951        default_shell_count = shell_par.default
[13dd7d2]2952        shell_min = 0
2953        shell_max = 0
2954        try:
2955            shell_min = int(shell_par.limits[0])
2956            shell_max = int(shell_par.limits[1])
2957        except IndexError as ex:
2958            # no info about limits
2959            pass
2960        item3.setText(str(shell_min))
2961        item4.setText(str(shell_max))
2962
2963        # Respond to index change
2964        func.currentTextChanged.connect(self.modifyShellsInList)
2965
[86d3207]2966        # Respond to button press
2967        button.clicked.connect(self.onShowSLDProfile)
2968
[13dd7d2]2969        # Available range of shells displayed in the combobox
2970        func.addItems([str(i) for i in range(shell_min, shell_max+1)])
[f712bf30]2971
2972        # Add default number of shells to the model
[13dd7d2]2973        func.setCurrentText(str(default_shell_count))
[a9b568c]2974
[13dd7d2]2975    def modifyShellsInList(self, text):
[86f88d1]2976        """
2977        Add/remove additional multishell parameters
2978        """
2979        # Find row location of the combobox
[70f4458]2980        first_row = self._n_shells_row + 1
2981        remove_rows = self._num_shell_params
[13dd7d2]2982        try:
2983            index = int(text)
2984        except ValueError:
2985            # bad text on the control!
2986            index = 0
2987            logger.error("Multiplicity incorrect! Setting to 0")
[86d3207]2988        self.kernel_module.multiplicity = index
[86f88d1]2989        if remove_rows > 1:
[70f4458]2990            self._model_model.removeRows(first_row, remove_rows)
2991
[b69b549]2992        new_rows = FittingUtilities.addShellsToModel(
[88ada06]2993                self.model_parameters,
2994                self._model_model,
[a758043]2995                index,
[b69b549]2996                first_row,
[a758043]2997                self.lstParams)
[86f88d1]2998
[b69b549]2999        self._num_shell_params = len(new_rows)
[a9b568c]3000        self.current_shell_displayed = index
[60af928]3001
[daf7c9c]3002        # Param values for existing shells were reset to default; force all changes into kernel module
3003        for row in new_rows:
3004            par = row[0].text()
3005            val = GuiUtils.toDouble(row[1].text())
3006            self.kernel_module.setParam(par, val)
3007
3008        # Change 'n' in the parameter model; also causes recalculation
[3090270]3009        self._model_model.item(self._n_shells_row, 1).setText(str(index))
3010
[0d13814]3011        # Update relevant models
3012        self.setPolyModel()
3013        self.setMagneticModel()
3014
[86d3207]3015    def onShowSLDProfile(self):
3016        """
3017        Show a quick plot of SLD profile
3018        """
3019        # get profile data
3020        x, y = self.kernel_module.getProfile()
3021        y *= 1.0e6
3022        profile_data = Data1D(x=x, y=y)
3023        profile_data.name = "SLD"
3024        profile_data.scale = 'linear'
3025        profile_data.symbol = 'Line'
3026        profile_data.hide_error = True
3027        profile_data._xaxis = "R(\AA)"
3028        profile_data._yaxis = "SLD(10^{-6}\AA^{-2})"
3029
3030        plotter = PlotterWidget(self, quickplot=True)
3031        plotter.data = profile_data
3032        plotter.showLegend = True
3033        plotter.plot(hide_error=True, marker='-')
3034
3035        self.plot_widget = QtWidgets.QWidget()
3036        self.plot_widget.setWindowTitle("Scattering Length Density Profile")
3037        layout = QtWidgets.QVBoxLayout()
3038        layout.addWidget(plotter)
3039        self.plot_widget.setLayout(layout)
3040        self.plot_widget.show()
3041
[557fc498]3042    def setInteractiveElements(self, enabled=True):
[14ec91c5]3043        """
[557fc498]3044        Switch interactive GUI elements on/off
[14ec91c5]3045        """
[557fc498]3046        assert isinstance(enabled, bool)
3047
3048        self.lstParams.setEnabled(enabled)
3049        self.lstPoly.setEnabled(enabled)
3050        self.lstMagnetic.setEnabled(enabled)
3051
3052        self.cbCategory.setEnabled(enabled)
3053        self.cbModel.setEnabled(enabled)
3054        self.cbStructureFactor.setEnabled(enabled)
[14ec91c5]3055
[557fc498]3056        self.chkPolydispersity.setEnabled(enabled)
3057        self.chkMagnetism.setEnabled(enabled)
3058        self.chk2DView.setEnabled(enabled)
3059
3060    def enableInteractiveElements(self):
[14ec91c5]3061        """
[557fc498]3062        Set buttion caption on fitting/calculate finish
3063        Enable the param table(s)
[14ec91c5]3064        """
[ded5e77]3065        # Notify the user that fitting is available
3066        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
[14ec91c5]3067        self.cmdFit.setText("Fit")
[ded5e77]3068        self.fit_started = False
[557fc498]3069        self.setInteractiveElements(True)
3070
3071    def disableInteractiveElements(self):
3072        """
3073        Set buttion caption on fitting/calculate start
3074        Disable the param table(s)
3075        """
3076        # Notify the user that fitting is being run
3077        # Allow for stopping the job
3078        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
3079        self.cmdFit.setText('Stop fit')
3080        self.setInteractiveElements(False)
[14ec91c5]3081
[672b8ab]3082    def readFitPage(self, fp):
3083        """
3084        Read in state from a fitpage object and update GUI
3085        """
3086        assert isinstance(fp, FitPage)
3087        # Main tab info
3088        self.logic.data.filename = fp.filename
3089        self.data_is_loaded = fp.data_is_loaded
3090        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
3091        self.chkMagnetism.setCheckState(fp.is_magnetic)
3092        self.chk2DView.setCheckState(fp.is2D)
3093
3094        # Update the comboboxes
3095        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
3096        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
3097        if fp.current_factor:
3098            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
3099
3100        self.chi2 = fp.chi2
3101
3102        # Options tab
3103        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
3104        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
3105        self.npts = fp.fit_options[fp.NPTS]
3106        self.log_points = fp.fit_options[fp.LOG_POINTS]
3107        self.weighting = fp.fit_options[fp.WEIGHTING]
3108
3109        # Models
[d60da0c]3110        self._model_model = fp.model_model
3111        self._poly_model = fp.poly_model
3112        self._magnet_model = fp.magnetism_model
[672b8ab]3113
3114        # Resolution tab
3115        smearing = fp.smearing_options[fp.SMEARING_OPTION]
3116        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
3117        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
3118        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
3119        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
3120
3121        # TODO: add polidyspersity and magnetism
3122
3123    def saveToFitPage(self, fp):
3124        """
3125        Write current state to the given fitpage
3126        """
3127        assert isinstance(fp, FitPage)
3128
3129        # Main tab info
3130        fp.filename = self.logic.data.filename
3131        fp.data_is_loaded = self.data_is_loaded
3132        fp.is_polydisperse = self.chkPolydispersity.isChecked()
3133        fp.is_magnetic = self.chkMagnetism.isChecked()
3134        fp.is2D = self.chk2DView.isChecked()
3135        fp.data = self.data
3136
3137        # Use current models - they contain all the required parameters
3138        fp.model_model = self._model_model
3139        fp.poly_model = self._poly_model
3140        fp.magnetism_model = self._magnet_model
3141
3142        if self.cbCategory.currentIndex() != 0:
3143            fp.current_category = str(self.cbCategory.currentText())
3144            fp.current_model = str(self.cbModel.currentText())
3145
3146        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
3147            fp.current_factor = str(self.cbStructureFactor.currentText())
3148        else:
3149            fp.current_factor = ''
3150
3151        fp.chi2 = self.chi2
[6dbff18]3152        fp.main_params_to_fit = self.main_params_to_fit
3153        fp.poly_params_to_fit = self.poly_params_to_fit
3154        fp.magnet_params_to_fit = self.magnet_params_to_fit
[6964d44]3155        fp.kernel_module = self.kernel_module
[672b8ab]3156
[6ff2eb3]3157        # Algorithm options
3158        # fp.algorithm = self.parent.fit_options.selected_id
3159
[672b8ab]3160        # Options tab
3161        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
3162        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
3163        fp.fit_options[fp.NPTS] = self.npts
3164        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
3165        fp.fit_options[fp.LOG_POINTS] = self.log_points
3166        fp.fit_options[fp.WEIGHTING] = self.weighting
3167
3168        # Resolution tab
3169        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
3170        fp.smearing_options[fp.SMEARING_OPTION] = smearing
3171        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
3172        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
3173        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
3174
3175        # TODO: add polidyspersity and magnetism
3176
[00b3b40]3177    def updateUndo(self):
3178        """
3179        Create a new state page and add it to the stack
3180        """
3181        if self.undo_supported:
3182            self.pushFitPage(self.currentState())
3183
[672b8ab]3184    def currentState(self):
3185        """
3186        Return fit page with current state
3187        """
3188        new_page = FitPage()
3189        self.saveToFitPage(new_page)
3190
3191        return new_page
3192
3193    def pushFitPage(self, new_page):
3194        """
3195        Add a new fit page object with current state
3196        """
[6011788]3197        self.page_stack.append(new_page)
[672b8ab]3198
3199    def popFitPage(self):
3200        """
3201        Remove top fit page from stack
3202        """
[6011788]3203        if self.page_stack:
3204            self.page_stack.pop()
[672b8ab]3205
[57be490]3206    def getReport(self):
3207        """
3208        Create and return HTML report with parameters and charts
3209        """
3210        index = None
3211        if self.all_data:
3212            index = self.all_data[self.data_index]
[cb90b65]3213        else:
3214            index = self.theory_item
[57be490]3215        report_logic = ReportPageLogic(self,
3216                                       kernel_module=self.kernel_module,
3217                                       data=self.data,
3218                                       index=index,
3219                                       model=self._model_model)
3220
3221        return report_logic.reportList()
3222
3223    def savePageState(self):
3224        """
3225        Create and serialize local PageState
3226        """
3227        from sas.sascalc.fit.pagestate import Reader
3228        model = self.kernel_module
3229
3230        # Old style PageState object
3231        state = PageState(model=model, data=self.data)
3232
3233        # Add parameter data to the state
3234        self.getCurrentFitState(state)
3235
3236        # Create the filewriter, aptly named 'Reader'
3237        state_reader = Reader(self.loadPageStateCallback)
3238        filepath = self.saveAsAnalysisFile()
[10fee37]3239        if filepath is None or filepath == "":
[57be490]3240            return
3241        state_reader.write(filename=filepath, fitstate=state)
3242        pass
3243
3244    def saveAsAnalysisFile(self):
3245        """
3246        Show the save as... dialog and return the chosen filepath
3247        """
3248        default_name = "FitPage"+str(self.tab_id)+".fitv"
3249
3250        wildcard = "fitv files (*.fitv)"
3251        kwargs = {
3252            'caption'   : 'Save As',
3253            'directory' : default_name,
3254            'filter'    : wildcard,
3255            'parent'    : None,
3256        }
3257        # Query user for filename.
3258        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
3259        filename = filename_tuple[0]
3260        return filename
3261
3262    def loadPageStateCallback(self,state=None, datainfo=None, format=None):
3263        """
3264        This is a callback method called from the CANSAS reader.
3265        We need the instance of this reader only for writing out a file,
3266        so there's nothing here.
3267        Until Load Analysis is implemented, that is.
3268        """
3269        pass
3270
3271    def loadPageState(self, pagestate=None):
3272        """
3273        Load the PageState object and update the current widget
3274        """
3275        pass
3276
3277    def getCurrentFitState(self, state=None):
3278        """
3279        Store current state for fit_page
3280        """
3281        # save model option
3282        #if self.model is not None:
3283        #    self.disp_list = self.getDispParamList()
3284        #    state.disp_list = copy.deepcopy(self.disp_list)
3285        #    #state.model = self.model.clone()
3286
3287        # Comboboxes
3288        state.categorycombobox = self.cbCategory.currentText()
3289        state.formfactorcombobox = self.cbModel.currentText()
3290        if self.cbStructureFactor.isEnabled():
[10fee37]3291            state.structurecombobox = self.cbStructureFactor.currentText()
[57be490]3292        state.tcChi = self.chi2
3293
3294        state.enable2D = self.is2D
3295
3296        #state.weights = copy.deepcopy(self.weights)
3297        # save data
3298        state.data = copy.deepcopy(self.data)
3299
3300        # save plotting range
3301        state.qmin = self.q_range_min
3302        state.qmax = self.q_range_max
3303        state.npts = self.npts
3304
3305        #    self.state.enable_disp = self.enable_disp.GetValue()
3306        #    self.state.disable_disp = self.disable_disp.GetValue()
3307
3308        #    self.state.enable_smearer = \
3309        #                        copy.deepcopy(self.enable_smearer.GetValue())
3310        #    self.state.disable_smearer = \
3311        #                        copy.deepcopy(self.disable_smearer.GetValue())
3312
3313        #self.state.pinhole_smearer = \
3314        #                        copy.deepcopy(self.pinhole_smearer.GetValue())
3315        #self.state.slit_smearer = copy.deepcopy(self.slit_smearer.GetValue())
3316        #self.state.dI_noweight = copy.deepcopy(self.dI_noweight.GetValue())
3317        #self.state.dI_didata = copy.deepcopy(self.dI_didata.GetValue())
3318        #self.state.dI_sqrdata = copy.deepcopy(self.dI_sqrdata.GetValue())
3319        #self.state.dI_idata = copy.deepcopy(self.dI_idata.GetValue())
3320
3321        p = self.model_parameters
3322        # save checkbutton state and txtcrtl values
[10fee37]3323        state.parameters = FittingUtilities.getStandardParam(self._model_model)
3324        state.orientation_params_disp = FittingUtilities.getOrientationParam(self.kernel_module)
[57be490]3325
3326        #self._copy_parameters_state(self.orientation_params_disp, self.state.orientation_params_disp)
3327        #self._copy_parameters_state(self.parameters, self.state.parameters)
3328        #self._copy_parameters_state(self.fittable_param, self.state.fittable_param)
3329        #self._copy_parameters_state(self.fixed_param, self.state.fixed_param)
3330
[8e2cd79]3331    def onParameterCopy(self, format=None):
3332        """
3333        Copy current parameters into the clipboard
3334        """
3335        # run a loop over all parameters and pull out
3336        # first - regular params
3337        param_list = []
[0eff615]3338
3339        param_list.append(['model_name', str(self.cbModel.currentText())])
[8e2cd79]3340        def gatherParams(row):
3341            """
3342            Create list of main parameters based on _model_model
3343            """
3344            param_name = str(self._model_model.item(row, 0).text())
3345            param_checked = str(self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3346            param_value = str(self._model_model.item(row, 1).text())
3347            param_error = None
[685602a]3348            param_min = None
3349            param_max = None
[8e2cd79]3350            column_offset = 0
3351            if self.has_error_column:
3352                param_error = str(self._model_model.item(row, 2).text())
3353                column_offset = 1
[685602a]3354
3355            try:
3356                param_min = str(self._model_model.item(row, 2+column_offset).text())
3357                param_max = str(self._model_model.item(row, 3+column_offset).text())
3358            except:
3359                pass
3360
[8e2cd79]3361            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3362
3363        def gatherPolyParams(row):
3364            """
3365            Create list of polydisperse parameters based on _poly_model
3366            """
3367            param_name = str(self._poly_model.item(row, 0).text()).split()[-1]
3368            param_checked = str(self._poly_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3369            param_value = str(self._poly_model.item(row, 1).text())
3370            param_error = None
3371            column_offset = 0
3372            if self.has_poly_error_column:
3373                param_error = str(self._poly_model.item(row, 2).text())
3374                column_offset = 1
3375            param_min   = str(self._poly_model.item(row, 2+column_offset).text())
3376            param_max   = str(self._poly_model.item(row, 3+column_offset).text())
3377            param_npts  = str(self._poly_model.item(row, 4+column_offset).text())
3378            param_nsigs = str(self._poly_model.item(row, 5+column_offset).text())
3379            param_fun   = str(self._poly_model.item(row, 6+column_offset).text()).rstrip()
3380            # width
3381            name = param_name+".width"
3382            param_list.append([name, param_checked, param_value, param_error,
3383                                param_npts, param_nsigs, param_min, param_max, param_fun])
3384
3385        def gatherMagnetParams(row):
3386            """
3387            Create list of magnetic parameters based on _magnet_model
3388            """
3389            param_name = str(self._magnet_model.item(row, 0).text())
3390            param_checked = str(self._magnet_model.item(row, 0).checkState() == QtCore.Qt.Checked)
3391            param_value = str(self._magnet_model.item(row, 1).text())
3392            param_error = None
3393            column_offset = 0
3394            if self.has_magnet_error_column:
3395                param_error = str(self._magnet_model.item(row, 2).text())
3396                column_offset = 1
3397            param_min = str(self._magnet_model.item(row, 2+column_offset).text())
3398            param_max = str(self._magnet_model.item(row, 3+column_offset).text())
3399            param_list.append([param_name, param_checked, param_value, param_error, param_min, param_max])
3400
3401        self.iterateOverModel(gatherParams)
3402        if self.chkPolydispersity.isChecked():
3403            self.iterateOverPolyModel(gatherPolyParams)
3404        if self.chkMagnetism.isChecked() and self.chkMagnetism.isEnabled():
[b259485]3405            self.iterateOverMagnetModel(gatherMagnetParams)
[8e2cd79]3406
3407        if format=="":
3408            formatted_output = FittingUtilities.formatParameters(param_list)
3409        elif format == "Excel":
[d4ba565]3410            formatted_output = FittingUtilities.formatParametersExcel(param_list[1:])
[8e2cd79]3411        elif format == "Latex":
[d4ba565]3412            formatted_output = FittingUtilities.formatParametersLatex(param_list[1:])
[8e2cd79]3413        else:
3414            raise AttributeError("Bad format specifier.")
3415
3416        # Dump formatted_output to the clipboard
3417        cb = QtWidgets.QApplication.clipboard()
3418        cb.setText(formatted_output)
3419
3420    def onParameterPaste(self):
3421        """
3422        Use the clipboard to update fit state
3423        """
3424        # Check if the clipboard contains right stuff
3425        cb = QtWidgets.QApplication.clipboard()
3426        cb_text = cb.text()
3427
3428        context = {}
3429        # put the text into dictionary
3430        lines = cb_text.split(':')
3431        if lines[0] != 'sasview_parameter_values':
3432            return False
[0eff615]3433
3434        model = lines[1].split(',')
3435
3436        if model[0] != 'model_name':
3437            return False
3438
3439        context['model_name'] = [model[1]]
3440        for line in lines[2:-1]:
[8e2cd79]3441            if len(line) != 0:
3442                item = line.split(',')
3443                check = item[1]
3444                name = item[0]
3445                value = item[2]
3446                # Transfer the text to content[dictionary]
3447                context[name] = [check, value]
3448
3449                # limits
[685602a]3450                try:
3451                    limit_lo = item[3]
3452                    context[name].append(limit_lo)
3453                    limit_hi = item[4]
3454                    context[name].append(limit_hi)
3455                except:
3456                    pass
[8e2cd79]3457
3458                # Polydisp
3459                if len(item) > 5:
3460                    value = item[5]
3461                    context[name].append(value)
3462                    try:
3463                        value = item[6]
3464                        context[name].append(value)
3465                        value = item[7]
3466                        context[name].append(value)
3467                    except IndexError:
3468                        pass
3469
[0eff615]3470        if str(self.cbModel.currentText()) != str(context['model_name'][0]):
3471            msg = QtWidgets.QMessageBox()
3472            msg.setIcon(QtWidgets.QMessageBox.Information)
3473            msg.setText("The model in the clipboard is not the same as the currently loaded model. \
3474                         Not all parameters saved may paste correctly.")
3475            msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
3476            result = msg.exec_()
3477            if result == QtWidgets.QMessageBox.Ok:
3478                pass
3479            else:
3480                return
3481
[8e2cd79]3482        self.updateFullModel(context)
3483        self.updateFullPolyModel(context)
3484
3485    def updateFullModel(self, param_dict):
3486        """
3487        Update the model with new parameters
3488        """
3489        assert isinstance(param_dict, dict)
3490        if not dict:
3491            return
3492
3493        def updateFittedValues(row):
3494            # Utility function for main model update
3495            # internal so can use closure for param_dict
3496            param_name = str(self._model_model.item(row, 0).text())
3497            if param_name not in list(param_dict.keys()):
3498                return
3499            # checkbox state
3500            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3501            self._model_model.item(row, 0).setCheckState(param_checked)
3502
3503            # modify the param value
3504            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3505            self._model_model.item(row, 1).setText(param_repr)
3506
3507            # Potentially the error column
3508            ioffset = 0
3509            if len(param_dict[param_name])>4 and self.has_error_column:
3510                # error values are not editable - no need to update
3511                #error_repr = GuiUtils.formatNumber(param_dict[param_name][2], high=True)
3512                #self._model_model.item(row, 2).setText(error_repr)
3513                ioffset = 1
3514            # min/max
[685602a]3515            try:
3516                param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3517                self._model_model.item(row, 2+ioffset).setText(param_repr)
3518                param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3519                self._model_model.item(row, 3+ioffset).setText(param_repr)
3520            except:
3521                pass
3522
[0eff615]3523            self.setFocus()
3524
[8e2cd79]3525
[685602a]3526
[8e2cd79]3527        # block signals temporarily, so we don't end up
3528        # updating charts with every single model change on the end of fitting
3529        self._model_model.blockSignals(True)
3530        self.iterateOverModel(updateFittedValues)
3531        self._model_model.blockSignals(False)
3532
[0eff615]3533
[8e2cd79]3534    def updateFullPolyModel(self, param_dict):
3535        """
3536        Update the polydispersity model with new parameters, create the errors column
3537        """
3538        assert isinstance(param_dict, dict)
3539        if not dict:
3540            return
3541
3542        def updateFittedValues(row):
3543            # Utility function for main model update
3544            # internal so can use closure for param_dict
3545            if row >= self._poly_model.rowCount():
3546                return
3547            param_name = str(self._poly_model.item(row, 0).text()).rsplit()[-1] + '.width'
3548            if param_name not in list(param_dict.keys()):
3549                return
3550            # checkbox state
3551            param_checked = QtCore.Qt.Checked if param_dict[param_name][0] == "True" else QtCore.Qt.Unchecked
3552            self._poly_model.item(row,0).setCheckState(param_checked)
3553
3554            # modify the param value
3555            param_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
3556            self._poly_model.item(row, 1).setText(param_repr)
3557
3558            # Potentially the error column
3559            ioffset = 0
3560            if len(param_dict[param_name])>4 and self.has_poly_error_column:
3561                ioffset = 1
3562            # min
3563            param_repr = GuiUtils.formatNumber(param_dict[param_name][2+ioffset], high=True)
3564            self._poly_model.item(row, 2+ioffset).setText(param_repr)
3565            # max
3566            param_repr = GuiUtils.formatNumber(param_dict[param_name][3+ioffset], high=True)
3567            self._poly_model.item(row, 3+ioffset).setText(param_repr)
3568            # Npts
3569            param_repr = GuiUtils.formatNumber(param_dict[param_name][4+ioffset], high=True)
3570            self._poly_model.item(row, 4+ioffset).setText(param_repr)
3571            # Nsigs
3572            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3573            self._poly_model.item(row, 5+ioffset).setText(param_repr)
3574
3575            param_repr = GuiUtils.formatNumber(param_dict[param_name][5+ioffset], high=True)
3576            self._poly_model.item(row, 5+ioffset).setText(param_repr)
[0eff615]3577            self.setFocus()
[8e2cd79]3578
3579        # block signals temporarily, so we don't end up
3580        # updating charts with every single model change on the end of fitting
3581        self._poly_model.blockSignals(True)
3582        self.iterateOverPolyModel(updateFittedValues)
3583        self._poly_model.blockSignals(False)
3584
[339e22b]3585
3586
Note: See TracBrowser for help on using the repository browser.