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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 2dce1d8 was 2dce1d8, checked in by rozyczko <piotr.rozyczko@…>, 6 years ago

Don't display plugin model category if there are not plugins defined.
SASVIEW-1123

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