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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 9a7c81c was 9a7c81c, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Fixed smearing for 1 and 2D

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