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

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

Fixed naming of datasets for both theory and file data

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