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

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 57be490 was 57be490, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Merged ESS_GUI_reporting

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