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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 04972ea was 04972ea, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

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

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