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

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

plotPolydispersities(): use precalc. weights from SasviewModel?

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