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

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

Added constraints to the fitter

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