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

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

Updated references to help files

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