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

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

Simple vs. complex constraints behaviour fixed.

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