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

ESS_GUIESS_GUI_opencl
Last change on this file since 73fb503 was 73fb503, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Allow for smearing selection on theory plots.

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