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

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

Minor fixes in fitpage

  • Property mode set to 100644
File size: 49.6 KB
Line 
1import json
2import os
3from collections import defaultdict
4from itertools import izip
5
6import logging
7import traceback
8from twisted.internet import threads
9import numpy as np
10
11from PyQt4 import QtGui
12from PyQt4 import QtCore
13from PyQt4 import QtWebKit
14
15from sasmodels import generate
16from sasmodels import modelinfo
17from sasmodels.sasview_model import load_standard_models
18from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
19
20import sas.qtgui.Utilities.GuiUtils as GuiUtils
21from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
22from sas.qtgui.Plotting.PlotterData import Data1D
23from sas.qtgui.Plotting.PlotterData import Data2D
24
25from sas.qtgui.Perspectives.Fitting.UI.FittingWidgetUI import Ui_FittingWidgetUI
26from sas.qtgui.Perspectives.Fitting.FitThread import FitThread
27from sas.qtgui.Perspectives.Fitting.ModelThread import Calc1D
28from sas.qtgui.Perspectives.Fitting.ModelThread import Calc2D
29from sas.qtgui.Perspectives.Fitting.FittingLogic import FittingLogic
30from sas.qtgui.Perspectives.Fitting import FittingUtilities
31from sas.qtgui.Perspectives.Fitting.SmearingWidget import SmearingWidget
32from sas.qtgui.Perspectives.Fitting.OptionsWidget import OptionsWidget
33from sas.qtgui.Perspectives.Fitting.FitPage import FitPage
34from sas.qtgui.Perspectives.Fitting.ViewDelegate import ModelViewDelegate
35from sas.qtgui.Perspectives.Fitting.ViewDelegate import PolyViewDelegate
36
37TAB_MAGNETISM = 4
38TAB_POLY = 3
39CATEGORY_DEFAULT = "Choose category..."
40CATEGORY_STRUCTURE = "Structure Factor"
41STRUCTURE_DEFAULT = "None"
42
43class FittingWidget(QtGui.QWidget, Ui_FittingWidgetUI):
44    """
45    Main widget for selecting form and structure factor models
46    """
47    def __init__(self, parent=None, data=None, tab_id=1):
48
49        super(FittingWidget, self).__init__()
50
51        # Necessary globals
52        self.parent = parent
53
54        # Which tab is this widget displayed in?
55        self.tab_id = tab_id
56
57        # Main Data[12]D holder
58        self.logic = FittingLogic(data=data)
59
60        # Globals
61        self.initializeGlobals()
62
63        # Main GUI setup up
64        self.setupUi(self)
65        self.setWindowTitle("Fitting")
66
67        # Set up tabs widgets
68        self.initializeWidgets()
69
70        # Set up models and views
71        self.initializeModels()
72
73        # Defaults for the structure factors
74        self.setDefaultStructureCombo()
75
76        # Make structure factor and model CBs disabled
77        self.disableModelCombo()
78        self.disableStructureCombo()
79
80        # Generate the category list for display
81        self.initializeCategoryCombo()
82
83        # Connect signals to controls
84        self.initializeSignals()
85
86        # Initial control state
87        self.initializeControls()
88
89        # Display HTML content
90        self.helpView = QtWebKit.QWebView()
91
92        self._index = None
93        if data is not None:
94            self.data = data
95
96    def close(self):
97        """
98        Remember to kill off things on exit
99        """
100        self.helpView.close()
101        del self.helpView
102
103    @property
104    def data(self):
105        return self.logic.data
106
107    @data.setter
108    def data(self, value):
109        """ data setter """
110        assert isinstance(value, QtGui.QStandardItem)
111        # _index contains the QIndex with data
112        self._index = value
113
114        # Update logics with data items
115        self.logic.data = GuiUtils.dataFromItem(value)
116
117        # Overwrite data type descriptor
118        self.is2D = True if isinstance(self.logic.data, Data2D) else False
119
120        self.data_is_loaded = True
121
122        # Enable/disable UI components
123        self.setEnablementOnDataLoad()
124
125    def initializeGlobals(self):
126        """
127        Initialize global variables used in this class
128        """
129        # SasModel is loaded
130        self.model_is_loaded = False
131        # Data[12]D passed and set
132        self.data_is_loaded = False
133        # Current SasModel in view
134        self.kernel_module = None
135        # Current SasModel view dimension
136        self.is2D = False
137        # Current SasModel is multishell
138        self.model_has_shells = False
139        # Utility variable to enable unselectable option in category combobox
140        self._previous_category_index = 0
141        # Utility variable for multishell display
142        self._last_model_row = 0
143        # Dictionary of {model name: model class} for the current category
144        self.models = {}
145        # Parameters to fit
146        self.parameters_to_fit = None
147        # Fit options
148        self.q_range_min = 0.005
149        self.q_range_max = 0.1
150        self.npts = 25
151        self.log_points = False
152        self.weighting = 0
153        self.chi2 = None
154        # Does the control support UNDO/REDO
155        # temporarily off
156        self.undo_supported = False
157        self.page_stack = []
158
159        # Data for chosen model
160        self.model_data = None
161
162        # Which shell is being currently displayed?
163        self.current_shell_displayed = 0
164        self.has_error_column = False
165
166        # signal communicator
167        self.communicate = self.parent.communicate
168
169    def initializeWidgets(self):
170        """
171        Initialize widgets for tabs
172        """
173        # Options widget
174        layout = QtGui.QGridLayout()
175        self.options_widget = OptionsWidget(self, self.logic)
176        layout.addWidget(self.options_widget)
177        self.tabOptions.setLayout(layout)
178
179        # Smearing widget
180        layout = QtGui.QGridLayout()
181        self.smearing_widget = SmearingWidget(self)
182        layout.addWidget(self.smearing_widget)
183        self.tabResolution.setLayout(layout)
184
185        # Define bold font for use in various controls
186        self.boldFont = QtGui.QFont()
187        self.boldFont.setBold(True)
188
189        # Set data label
190        self.label.setFont(self.boldFont)
191        self.label.setText("No data loaded")
192        self.lblFilename.setText("")
193
194    def initializeModels(self):
195        """
196        Set up models and views
197        """
198        # Set the main models
199        # We can't use a single model here, due to restrictions on flattening
200        # the model tree with subclassed QAbstractProxyModel...
201        self._model_model = QtGui.QStandardItemModel()
202        self._poly_model = QtGui.QStandardItemModel()
203        self._magnet_model = QtGui.QStandardItemModel()
204
205        # Param model displayed in param list
206        self.lstParams.setModel(self._model_model)
207        self.readCategoryInfo()
208        self.model_parameters = None
209
210        # Delegates for custom editing and display
211        self.lstParams.setItemDelegate(ModelViewDelegate(self))
212
213        self.lstParams.setAlternatingRowColors(True)
214        stylesheet = """
215            QTreeView::item:hover {
216                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1);
217                border: 1px solid #bfcde4;
218            }
219
220            QTreeView::item:selected {
221                border: 1px solid #567dbc;
222            }
223
224            QTreeView::item:selected:active{
225                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6ea1f1, stop: 1 #567dbc);
226            }
227
228            QTreeView::item:selected:!active {
229                background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #6b9be8, stop: 1 #577fbf);
230            }
231           """
232        self.lstParams.setStyleSheet(stylesheet)
233        self.lstParams.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
234        self.lstParams.customContextMenuRequested.connect(self.showModelDescription)
235
236        # Poly model displayed in poly list
237        self.lstPoly.setModel(self._poly_model)
238        self.setPolyModel()
239        self.setTableProperties(self.lstPoly)
240        # Delegates for custom editing and display
241        self.lstPoly.setItemDelegate(PolyViewDelegate(self))
242
243        # Magnetism model displayed in magnetism list
244        self.lstMagnetic.setModel(self._magnet_model)
245        self.setMagneticModel()
246        self.setTableProperties(self.lstMagnetic)
247
248    def initializeCategoryCombo(self):
249        """
250        Model category combo setup
251        """
252        category_list = sorted(self.master_category_dict.keys())
253        self.cbCategory.addItem(CATEGORY_DEFAULT)
254        self.cbCategory.addItems(category_list)
255        self.cbCategory.addItem(CATEGORY_STRUCTURE)
256        self.cbCategory.setCurrentIndex(0)
257
258    def setEnablementOnDataLoad(self):
259        """
260        Enable/disable various UI elements based on data loaded
261        """
262        # Tag along functionality
263        self.label.setText("Data loaded from: ")
264        self.lblFilename.setText(self.logic.data.filename)
265        self.updateQRange()
266        # Switch off Data2D control
267        self.chk2DView.setEnabled(False)
268        self.chk2DView.setVisible(False)
269        self.chkMagnetism.setEnabled(True)
270        # Similarly on other tabs
271        self.options_widget.setEnablementOnDataLoad()
272
273        # Smearing tab
274        self.smearing_widget.updateSmearing(self.data)
275
276    def acceptsData(self):
277        """ Tells the caller this widget can accept new dataset """
278        return not self.data_is_loaded
279
280    def disableModelCombo(self):
281        """ Disable the combobox """
282        self.cbModel.setEnabled(False)
283        self.lblModel.setEnabled(False)
284
285    def enableModelCombo(self):
286        """ Enable the combobox """
287        self.cbModel.setEnabled(True)
288        self.lblModel.setEnabled(True)
289
290    def disableStructureCombo(self):
291        """ Disable the combobox """
292        self.cbStructureFactor.setEnabled(False)
293        self.lblStructure.setEnabled(False)
294
295    def enableStructureCombo(self):
296        """ Enable the combobox """
297        self.cbStructureFactor.setEnabled(True)
298        self.lblStructure.setEnabled(True)
299
300    def togglePoly(self, isChecked):
301        """ Enable/disable the polydispersity tab """
302        self.tabFitting.setTabEnabled(TAB_POLY, isChecked)
303
304    def toggleMagnetism(self, isChecked):
305        """ Enable/disable the magnetism tab """
306        self.tabFitting.setTabEnabled(TAB_MAGNETISM, isChecked)
307
308    def toggle2D(self, isChecked):
309        """ Enable/disable the controls dependent on 1D/2D data instance """
310        self.chkMagnetism.setEnabled(isChecked)
311        self.is2D = isChecked
312        # Reload the current model
313        if self.kernel_module:
314            self.onSelectModel()
315
316    def initializeControls(self):
317        """
318        Set initial control enablement
319        """
320        self.cmdFit.setEnabled(False)
321        self.cmdPlot.setEnabled(False)
322        self.options_widget.cmdComputePoints.setVisible(False) # probably redundant
323        self.chkPolydispersity.setEnabled(True)
324        self.chkPolydispersity.setCheckState(False)
325        self.chk2DView.setEnabled(True)
326        self.chk2DView.setCheckState(False)
327        self.chkMagnetism.setEnabled(False)
328        self.chkMagnetism.setCheckState(False)
329        # Tabs
330        self.tabFitting.setTabEnabled(TAB_POLY, False)
331        self.tabFitting.setTabEnabled(TAB_MAGNETISM, False)
332        self.lblChi2Value.setText("---")
333        # Smearing tab
334        self.smearing_widget.updateSmearing(self.data)
335        # Line edits in the option tab
336        self.updateQRange()
337
338    def initializeSignals(self):
339        """
340        Connect GUI element signals
341        """
342        # Comboboxes
343        self.cbStructureFactor.currentIndexChanged.connect(self.onSelectStructureFactor)
344        self.cbCategory.currentIndexChanged.connect(self.onSelectCategory)
345        self.cbModel.currentIndexChanged.connect(self.onSelectModel)
346        # Checkboxes
347        self.chk2DView.toggled.connect(self.toggle2D)
348        self.chkPolydispersity.toggled.connect(self.togglePoly)
349        self.chkMagnetism.toggled.connect(self.toggleMagnetism)
350        # Buttons
351        self.cmdFit.clicked.connect(self.onFit)
352        self.cmdPlot.clicked.connect(self.onPlot)
353        self.cmdHelp.clicked.connect(self.onHelp)
354
355        # Respond to change in parameters from the UI
356        self._model_model.itemChanged.connect(self.updateParamsFromModel)
357        self._poly_model.itemChanged.connect(self.onPolyModelChange)
358        # TODO after the poly_model prototype accepted
359        #self._magnet_model.itemChanged.connect(self.onMagneticModelChange)
360
361        # Signals from separate tabs asking for replot
362        self.options_widget.plot_signal.connect(self.onOptionsUpdate)
363
364    def showModelDescription(self, position):
365        """
366        Shows a window with model description, when right clicked in the treeview
367        """
368        msg = 'Model description:\n'
369        if self.kernel_module is not None:
370            if str(self.kernel_module.description).rstrip().lstrip() == '':
371                msg += "Sorry, no information is available for this model."
372            else:
373                msg += self.kernel_module.description + '\n'
374        else:
375            msg += "You must select a model to get information on this"
376
377        menu = QtGui.QMenu()
378        label = QtGui.QLabel(msg)
379        action = QtGui.QWidgetAction(self)
380        action.setDefaultWidget(label)
381        menu.addAction(action)
382        menu.exec_(self.lstParams.viewport().mapToGlobal(position))
383
384    def onSelectModel(self):
385        """
386        Respond to select Model from list event
387        """
388        model = str(self.cbModel.currentText())
389
390        # Reset structure factor
391        self.cbStructureFactor.setCurrentIndex(0)
392
393        # Reset parameters to fit
394        self.parameters_to_fit = None
395        self.has_error_column = False
396
397        self.respondToModelStructure(model=model, structure_factor=None)
398
399    def onSelectStructureFactor(self):
400        """
401        Select Structure Factor from list
402        """
403        model = str(self.cbModel.currentText())
404        category = str(self.cbCategory.currentText())
405        structure = str(self.cbStructureFactor.currentText())
406        if category == CATEGORY_STRUCTURE:
407            model = None
408        self.respondToModelStructure(model=model, structure_factor=structure)
409
410    def respondToModelStructure(self, model=None, structure_factor=None):
411        # Set enablement on calculate/plot
412        self.cmdPlot.setEnabled(True)
413
414        # kernel parameters -> model_model
415        self.SASModelToQModel(model, structure_factor)
416
417        if self.data_is_loaded:
418            self.cmdPlot.setText("Show Plot")
419            self.calculateQGridForModel()
420        else:
421            self.cmdPlot.setText("Calculate")
422            # Create default datasets if no data passed
423            self.createDefaultDataset()
424
425        # Update state stack
426        self.updateUndo()
427
428    def onSelectCategory(self):
429        """
430        Select Category from list
431        """
432        category = str(self.cbCategory.currentText())
433        # Check if the user chose "Choose category entry"
434        if category == CATEGORY_DEFAULT:
435            # if the previous category was not the default, keep it.
436            # Otherwise, just return
437            if self._previous_category_index != 0:
438                # We need to block signals, or else state changes on perceived unchanged conditions
439                self.cbCategory.blockSignals(True)
440                self.cbCategory.setCurrentIndex(self._previous_category_index)
441                self.cbCategory.blockSignals(False)
442            return
443
444        if category == CATEGORY_STRUCTURE:
445            self.disableModelCombo()
446            self.enableStructureCombo()
447            self._model_model.clear()
448            return
449
450        # Safely clear and enable the model combo
451        self.cbModel.blockSignals(True)
452        self.cbModel.clear()
453        self.cbModel.blockSignals(False)
454        self.enableModelCombo()
455        self.disableStructureCombo()
456
457        self._previous_category_index = self.cbCategory.currentIndex()
458        # Retrieve the list of models
459        model_list = self.master_category_dict[category]
460        # Populate the models combobox
461        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
462
463    def onPolyModelChange(self, item):
464        """
465        Callback method for updating the main model and sasmodel
466        parameters with the GUI values in the polydispersity view
467        """
468        model_column = item.column()
469        model_row = item.row()
470        name_index = self._poly_model.index(model_row, 0)
471        # Extract changed value. Assumes proper validation by QValidator/Delegate
472        # TODO: abstract away hardcoded column numbers
473        if model_column == 0:
474            # Is the parameter checked for fitting?
475            value = item.checkState()
476            # TODO: add the param to self.params_for_fitting
477        elif model_column == 6:
478            value = item.text()
479            # TODO: Modify Npts/Nsigs based on function choice
480        else:
481            try:
482                value = float(item.text())
483            except ValueError:
484                # Can't be converted properly, bring back the old value and exit
485                return
486
487        parameter_name = str(self._poly_model.data(name_index).toPyObject()) # "distribution of sld" etc.
488        if "Distribution of" in parameter_name:
489            parameter_name = parameter_name[16:]
490        property_name = str(self._poly_model.headerData(model_column, 1).toPyObject()) # Value, min, max, etc.
491        # print "%s(%s) => %d" % (parameter_name, property_name, value)
492
493        # Update the sasmodel
494        #self.kernel_module.params[parameter_name] = value
495
496        # Reload the main model - may not be required if no variable is shown in main view
497        #model = str(self.cbModel.currentText())
498        #self.SASModelToQModel(model)
499
500        pass # debug anchor
501
502    def onHelp(self):
503        """
504        Show the "Fitting" section of help
505        """
506        tree_location = self.parent.HELP_DIRECTORY_LOCATION +\
507            "/user/sasgui/perspectives/fitting/fitting_help.html"
508        self.helpView.load(QtCore.QUrl(tree_location))
509        self.helpView.show()
510
511    def onFit(self):
512        """
513        Perform fitting on the current data
514        """
515        fitter = Fit()
516
517        # Data going in
518        data = self.logic.data
519        model = self.kernel_module
520        qmin = self.q_range_min
521        qmax = self.q_range_max
522        params_to_fit = self.parameters_to_fit
523
524        # Potential weights added directly to data
525        self.addWeightingToData(data)
526
527        # Potential smearing added
528        # Remember that smearing_min/max can be None ->
529        # deal with it until Python gets discriminated unions
530        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
531
532        # These should be updating somehow?
533        fit_id = 0
534        constraints = []
535        smearer = None
536        page_id = [210]
537        handler = None
538        batch_inputs = {}
539        batch_outputs = {}
540        list_page_id = [page_id]
541        #---------------------------------
542
543        # Parameterize the fitter
544        fitter.set_model(model, fit_id, params_to_fit, data=data,
545                         constraints=constraints)
546
547        fitter.set_data(data=data, id=fit_id, smearer=smearer, qmin=qmin,
548                        qmax=qmax)
549        fitter.select_problem_for_fit(id=fit_id, value=1)
550
551        fitter.fitter_id = page_id
552
553        # Create the fitting thread, based on the fitter
554        calc_fit = FitThread(handler=handler,
555                             fn=[fitter],
556                             batch_inputs=batch_inputs,
557                             batch_outputs=batch_outputs,
558                             page_id=list_page_id,
559                             updatefn=self.updateFit,
560                             completefn=None)
561
562        # start the trhrhread
563        calc_thread = threads.deferToThread(calc_fit.compute)
564        calc_thread.addCallback(self.fitComplete)
565        calc_thread.addErrback(self.fitFailed)
566
567        #disable the Fit button
568        self.cmdFit.setText('Calculating...')
569        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
570        self.cmdFit.setEnabled(False)
571
572    def updateFit(self):
573        """
574        """
575        print "UPDATE FIT"
576        pass
577
578    def fitFailed(self, reason):
579        """
580        """
581        print "FIT FAILED: ", reason
582        pass
583
584    def fitComplete(self, result):
585        """
586        Receive and display fitting results
587        "result" is a tuple of actual result list and the fit time in seconds
588        """
589        #re-enable the Fit button
590        self.cmdFit.setText("Fit")
591        self.cmdFit.setEnabled(True)
592
593        assert result is not None
594
595        res_list = result[0]
596        res = res_list[0]
597        if res.fitness is None or \
598            not np.isfinite(res.fitness) or \
599            np.any(res.pvec is None) or \
600            not np.all(np.isfinite(res.pvec)):
601            msg = "Fitting did not converge!!!"
602            self.communicate.statusBarUpdateSignal.emit(msg)
603            logging.error(msg)
604            return
605
606        elapsed = result[1]
607        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
608        self.communicate.statusBarUpdateSignal.emit(msg)
609
610        self.chi2 = res.fitness
611        param_list = res.param_list
612        param_values = res.pvec
613        param_stderr = res.stderr
614        params_and_errors = zip(param_values, param_stderr)
615        param_dict = dict(izip(param_list, params_and_errors))
616
617        # Dictionary of fitted parameter: value, error
618        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
619        self.updateModelFromList(param_dict)
620
621        # update charts
622        self.onPlot()
623
624        # Read only value - we can get away by just printing it here
625        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
626        self.lblChi2Value.setText(chi2_repr)
627
628    def iterateOverModel(self, func):
629        """
630        Take func and throw it inside the model row loop
631        """
632        #assert isinstance(func, function)
633        for row_i in xrange(self._model_model.rowCount()):
634            func(row_i)
635
636    def updateModelFromList(self, param_dict):
637        """
638        Update the model with new parameters, create the errors column
639        """
640        assert isinstance(param_dict, dict)
641        if not dict:
642            return
643
644        def updateFittedValues(row_i):
645            # Utility function for main model update
646            # internal so can use closure for param_dict
647            param_name = str(self._model_model.item(row_i, 0).text())
648            if param_name not in param_dict.keys():
649                return
650            # modify the param value
651            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
652            self._model_model.item(row_i, 1).setText(param_repr)
653            if self.has_error_column:
654                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
655                self._model_model.item(row_i, 2).setText(error_repr)
656
657        def createErrorColumn(row_i):
658            # Utility function for error column update
659            item = QtGui.QStandardItem()
660            for param_name in param_dict.keys():
661                if str(self._model_model.item(row_i, 0).text()) != param_name:
662                    continue
663                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
664                item.setText(error_repr)
665            error_column.append(item)
666
667        # block signals temporarily, so we don't end up
668        # updating charts with every single model change on the end of fitting
669        self._model_model.blockSignals(True)
670        self.iterateOverModel(updateFittedValues)
671        self._model_model.blockSignals(False)
672
673        if self.has_error_column:
674            return
675
676        error_column = []
677        self.iterateOverModel(createErrorColumn)
678
679        # switch off reponse to model change
680        self._model_model.blockSignals(True)
681        self._model_model.insertColumn(2, error_column)
682        self._model_model.blockSignals(False)
683        FittingUtilities.addErrorHeadersToModel(self._model_model)
684        # Adjust the table cells width.
685        # TODO: find a way to dynamically adjust column width while resized expanding
686        self.lstParams.resizeColumnToContents(0)
687        self.lstParams.resizeColumnToContents(4)
688        self.lstParams.resizeColumnToContents(5)
689        self.lstParams.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
690
691        self.has_error_column = True
692
693    def onPlot(self):
694        """
695        Plot the current set of data
696        """
697        # Regardless of previous state, this should now be `plot show` functionality only
698        self.cmdPlot.setText("Show Plot")
699        if not self.data_is_loaded:
700            self.recalculatePlotData()
701        self.showPlot()
702
703    def recalculatePlotData(self):
704        """
705        Generate a new dataset for model
706        """
707        if not self.data_is_loaded:
708            self.createDefaultDataset()
709        self.calculateQGridForModel()
710
711    def showPlot(self):
712        """
713        Show the current plot in MPL
714        """
715        # Show the chart if ready
716        data_to_show = self.data if self.data_is_loaded else self.model_data
717        if data_to_show is not None:
718            self.communicate.plotRequestedSignal.emit([data_to_show])
719
720    def onOptionsUpdate(self):
721        """
722        Update local option values and replot
723        """
724        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
725            self.options_widget.state()
726        # set Q range labels on the main tab
727        self.lblMinRangeDef.setText(str(self.q_range_min))
728        self.lblMaxRangeDef.setText(str(self.q_range_max))
729        self.recalculatePlotData()
730
731    def setDefaultStructureCombo(self):
732        """
733        Fill in the structure factors combo box with defaults
734        """
735        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
736        factors = [factor[0] for factor in structure_factor_list]
737        factors.insert(0, STRUCTURE_DEFAULT)
738        self.cbStructureFactor.clear()
739        self.cbStructureFactor.addItems(sorted(factors))
740
741    def createDefaultDataset(self):
742        """
743        Generate default Dataset 1D/2D for the given model
744        """
745        # Create default datasets if no data passed
746        if self.is2D:
747            qmax = self.q_range_max/np.sqrt(2)
748            qstep = self.npts
749            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
750            return
751        elif self.log_points:
752            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
753            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
754            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
755        else:
756            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
757                                   num=self.npts, endpoint=True)
758        self.logic.createDefault1dData(interval, self.tab_id)
759
760    def readCategoryInfo(self):
761        """
762        Reads the categories in from file
763        """
764        self.master_category_dict = defaultdict(list)
765        self.by_model_dict = defaultdict(list)
766        self.model_enabled_dict = defaultdict(bool)
767
768        categorization_file = CategoryInstaller.get_user_file()
769        if not os.path.isfile(categorization_file):
770            categorization_file = CategoryInstaller.get_default_file()
771        with open(categorization_file, 'rb') as cat_file:
772            self.master_category_dict = json.load(cat_file)
773            self.regenerateModelDict()
774
775        # Load the model dict
776        models = load_standard_models()
777        for model in models:
778            self.models[model.name] = model
779
780    def regenerateModelDict(self):
781        """
782        Regenerates self.by_model_dict which has each model name as the
783        key and the list of categories belonging to that model
784        along with the enabled mapping
785        """
786        self.by_model_dict = defaultdict(list)
787        for category in self.master_category_dict:
788            for (model, enabled) in self.master_category_dict[category]:
789                self.by_model_dict[model].append(category)
790                self.model_enabled_dict[model] = enabled
791
792    def addBackgroundToModel(self, model):
793        """
794        Adds background parameter with default values to the model
795        """
796        assert isinstance(model, QtGui.QStandardItemModel)
797        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
798        FittingUtilities.addCheckedListToModel(model, checked_list)
799        last_row = model.rowCount()-1
800        model.item(last_row, 0).setEditable(False)
801        model.item(last_row, 4).setEditable(False)
802
803    def addScaleToModel(self, model):
804        """
805        Adds scale parameter with default values to the model
806        """
807        assert isinstance(model, QtGui.QStandardItemModel)
808        checked_list = ['scale', '1.0', '0.0', 'inf', '']
809        FittingUtilities.addCheckedListToModel(model, checked_list)
810        last_row = model.rowCount()-1
811        model.item(last_row, 0).setEditable(False)
812        model.item(last_row, 4).setEditable(False)
813
814    def addWeightingToData(self, data):
815        """
816        Adds weighting contribution to fitting data
817        """
818        # Send original data for weighting
819        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
820        update_module = data.err_data if self.is2D else data.dy
821        # Overwrite relevant values in data
822        update_module = weight
823
824    def updateQRange(self):
825        """
826        Updates Q Range display
827        """
828        if self.data_is_loaded:
829            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
830        # set Q range labels on the main tab
831        self.lblMinRangeDef.setText(str(self.q_range_min))
832        self.lblMaxRangeDef.setText(str(self.q_range_max))
833        # set Q range labels on the options tab
834        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
835
836    def SASModelToQModel(self, model_name, structure_factor=None):
837        """
838        Setting model parameters into table based on selected category
839        """
840        # Crete/overwrite model items
841        self._model_model.clear()
842
843        # First, add parameters from the main model
844        if model_name is not None:
845            self.fromModelToQModel(model_name)
846
847        # Then, add structure factor derived parameters
848        if structure_factor is not None and structure_factor != "None":
849            if model_name is None:
850                # Instantiate the current sasmodel for SF-only models
851                self.kernel_module = self.models[structure_factor]()
852            self.fromStructureFactorToQModel(structure_factor)
853        else:
854            # Allow the SF combobox visibility for the given sasmodel
855            self.enableStructureFactorControl(structure_factor)
856
857        # Then, add multishells
858        if model_name is not None:
859            # Multishell models need additional treatment
860            self.addExtraShells()
861
862        # Add polydispersity to the model
863        self.setPolyModel()
864        # Add magnetic parameters to the model
865        self.setMagneticModel()
866
867        # Adjust the table cells width
868        self.lstParams.resizeColumnToContents(0)
869        self.lstParams.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
870
871        # Now we claim the model has been loaded
872        self.model_is_loaded = True
873
874        # (Re)-create headers
875        FittingUtilities.addHeadersToModel(self._model_model)
876        self.lstParams.header().setFont(self.boldFont)
877
878        # Update Q Ranges
879        self.updateQRange()
880
881    def fromModelToQModel(self, model_name):
882        """
883        Setting model parameters into QStandardItemModel based on selected _model_
884        """
885        kernel_module = generate.load_kernel_module(model_name)
886        self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
887
888        # Instantiate the current sasmodel
889        self.kernel_module = self.models[model_name]()
890
891        # Explicitly add scale and background with default values
892        temp_undo_state = self.undo_supported
893        self.undo_supported = False
894        self.addScaleToModel(self._model_model)
895        self.addBackgroundToModel(self._model_model)
896        self.undo_supported = temp_undo_state
897
898        # Update the QModel
899        new_rows = FittingUtilities.addParametersToModel(self.model_parameters, self.is2D)
900        for row in new_rows:
901            self._model_model.appendRow(row)
902        # Update the counter used for multishell display
903        self._last_model_row = self._model_model.rowCount()
904
905    def fromStructureFactorToQModel(self, structure_factor):
906        """
907        Setting model parameters into QStandardItemModel based on selected _structure factor_
908        """
909        structure_module = generate.load_kernel_module(structure_factor)
910        structure_parameters = modelinfo.make_parameter_table(getattr(structure_module, 'parameters', []))
911
912        new_rows = FittingUtilities.addSimpleParametersToModel(structure_parameters, self.is2D)
913        for row in new_rows:
914            self._model_model.appendRow(row)
915        # Update the counter used for multishell display
916        self._last_model_row = self._model_model.rowCount()
917
918    def updateParamsFromModel(self, item):
919        """
920        Callback method for updating the sasmodel parameters with the GUI values
921        """
922        model_column = item.column()
923
924        if model_column == 0:
925            self.checkboxSelected(item)
926            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
927            # Update state stack
928            self.updateUndo()
929            return
930
931        model_row = item.row()
932        name_index = self._model_model.index(model_row, 0)
933
934        # Extract changed value. Assumes proper validation by QValidator/Delegate
935        try:
936            value = float(item.text())
937        except ValueError:
938            # Unparsable field
939            return
940        parameter_name = str(self._model_model.data(name_index).toPyObject()) # sld, background etc.
941        property_index = self._model_model.headerData(1, model_column).toInt()[0]-1 # Value, min, max, etc.
942
943        # Update the parameter value - note: this supports +/-inf as well
944        self.kernel_module.params[parameter_name] = value
945
946        # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
947        self.kernel_module.details[parameter_name][property_index] = value
948
949        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
950        # TODO: multishell params in self.kernel_module.details[??] = value
951
952        # Force the chart update when actual parameters changed
953        if model_column == 1:
954            self.recalculatePlotData()
955
956        # Update state stack
957        self.updateUndo()
958
959    def checkboxSelected(self, item):
960        # Assure we're dealing with checkboxes
961        if not item.isCheckable():
962            return
963        status = item.checkState()
964
965        def isChecked(row):
966            return self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked
967
968        def isCheckable(row):
969            return self._model_model.item(row, 0).isCheckable()
970
971        # If multiple rows selected - toggle all of them, filtering uncheckable
972        rows = [s.row() for s in self.lstParams.selectionModel().selectedRows() if isCheckable(s.row())]
973
974        # Switch off signaling from the model to avoid recursion
975        self._model_model.blockSignals(True)
976        # Convert to proper indices and set requested enablement
977        _ = [self._model_model.item(row, 0).setCheckState(status) for row in rows]
978        self._model_model.blockSignals(False)
979
980        # update the list of parameters to fit
981        self.parameters_to_fit = [str(self._model_model.item(row_index, 0).text())
982                                  for row_index in xrange(self._model_model.rowCount())
983                                  if isChecked(row_index)]
984
985    def nameForFittedData(self, name):
986        """
987        Generate name for the current fit
988        """
989        if self.is2D:
990            name += "2d"
991        name = "M%i [%s]" % (self.tab_id, name)
992        return name
993
994    def createNewIndex(self, fitted_data):
995        """
996        Create a model or theory index with passed Data1D/Data2D
997        """
998        if self.data_is_loaded:
999            if not fitted_data.name:
1000                name = self.nameForFittedData(self.data.filename)
1001                fitted_data.title = name
1002                fitted_data.name = name
1003                fitted_data.filename = name
1004                fitted_data.symbol = "Line"
1005            self.updateModelIndex(fitted_data)
1006        else:
1007            name = self.nameForFittedData(self.kernel_module.name)
1008            fitted_data.title = name
1009            fitted_data.name = name
1010            fitted_data.filename = name
1011            fitted_data.symbol = "Line"
1012            self.createTheoryIndex(fitted_data)
1013
1014    def updateModelIndex(self, fitted_data):
1015        """
1016        Update a QStandardModelIndex containing model data
1017        """
1018        name = self.nameFromData(fitted_data)
1019        # Make this a line if no other defined
1020        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
1021            fitted_data.symbol = 'Line'
1022        # Notify the GUI manager so it can update the main model in DataExplorer
1023        GuiUtils.updateModelItemWithPlot(self._index, QtCore.QVariant(fitted_data), name)
1024
1025    def createTheoryIndex(self, fitted_data):
1026        """
1027        Create a QStandardModelIndex containing model data
1028        """
1029        name = self.nameFromData(fitted_data)
1030        # Notify the GUI manager so it can create the theory model in DataExplorer
1031        new_item = GuiUtils.createModelItemWithPlot(QtCore.QVariant(fitted_data), name=name)
1032        self.communicate.updateTheoryFromPerspectiveSignal.emit(new_item)
1033
1034    def nameFromData(self, fitted_data):
1035        """
1036        Return name for the dataset. Terribly impure function.
1037        """
1038        if fitted_data.name is None:
1039            name = self.nameForFittedData(self.logic.data.filename)
1040            fitted_data.title = name
1041            fitted_data.name = name
1042            fitted_data.filename = name
1043        else:
1044            name = fitted_data.name
1045        return name
1046
1047    def methodCalculateForData(self):
1048        '''return the method for data calculation'''
1049        return Calc1D if isinstance(self.data, Data1D) else Calc2D
1050
1051    def methodCompleteForData(self):
1052        '''return the method for result parsin on calc complete '''
1053        return self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1054
1055    def calculateQGridForModel(self):
1056        """
1057        Prepare the fitting data object, based on current ModelModel
1058        """
1059        if self.kernel_module is None:
1060            return
1061        # Awful API to a backend method.
1062        method = self.methodCalculateForData()(data=self.data,
1063                                               model=self.kernel_module,
1064                                               page_id=0,
1065                                               qmin=self.q_range_min,
1066                                               qmax=self.q_range_max,
1067                                               smearer=None,
1068                                               state=None,
1069                                               weight=None,
1070                                               fid=None,
1071                                               toggle_mode_on=False,
1072                                               completefn=None,
1073                                               update_chisqr=True,
1074                                               exception_handler=self.calcException,
1075                                               source=None)
1076
1077        calc_thread = threads.deferToThread(method.compute)
1078        calc_thread.addCallback(self.methodCompleteForData())
1079        calc_thread.addErrback(self.calculateDataFailed())
1080
1081    def calculateDataFailed(self):
1082        """
1083        """
1084        print "Calculate Data failed."
1085
1086    def complete1D(self, return_data):
1087        """
1088        Plot the current 1D data
1089        """
1090        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
1091        self.calculateResiduals(fitted_data)
1092        self.model_data = fitted_data
1093
1094    def complete2D(self, return_data):
1095        """
1096        Plot the current 2D data
1097        """
1098        fitted_data = self.logic.new2DPlot(return_data)
1099        self.calculateResiduals(fitted_data)
1100        self.model_data = fitted_data
1101
1102    def calculateResiduals(self, fitted_data):
1103        """
1104        Calculate and print Chi2 and display chart of residuals
1105        """
1106        # Create a new index for holding data
1107        fitted_data.symbol = "Line"
1108
1109        # Modify fitted_data with weighting
1110        self.addWeightingToData(fitted_data)
1111
1112        self.createNewIndex(fitted_data)
1113        # Calculate difference between return_data and logic.data
1114        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
1115        # Update the control
1116        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
1117        self.lblChi2Value.setText(chi2_repr)
1118
1119        self.communicate.plotUpdateSignal.emit([fitted_data])
1120
1121        # Plot residuals if actual data
1122        if self.data_is_loaded:
1123            residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
1124            residuals_plot.id = "Residual " + residuals_plot.id
1125            self.createNewIndex(residuals_plot)
1126            self.communicate.plotUpdateSignal.emit([residuals_plot])
1127
1128    def calcException(self, etype, value, tb):
1129        """
1130        Something horrible happened in the deferred.
1131        """
1132        logging.error("".join(traceback.format_exception(etype, value, tb)))
1133
1134    def setTableProperties(self, table):
1135        """
1136        Setting table properties
1137        """
1138        # Table properties
1139        table.verticalHeader().setVisible(False)
1140        table.setAlternatingRowColors(True)
1141        table.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
1142        table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
1143        table.resizeColumnsToContents()
1144
1145        # Header
1146        header = table.horizontalHeader()
1147        header.setResizeMode(QtGui.QHeaderView.ResizeToContents)
1148
1149        header.ResizeMode(QtGui.QHeaderView.Interactive)
1150        # Resize column 0 and 6 to content
1151        header.setResizeMode(0, QtGui.QHeaderView.ResizeToContents)
1152        header.setResizeMode(6, QtGui.QHeaderView.ResizeToContents)
1153
1154    def setPolyModel(self):
1155        """
1156        Set polydispersity values
1157        """
1158        if not self.model_parameters:
1159            return
1160        self._poly_model.clear()
1161        for row, param in enumerate(self.model_parameters.form_volume_parameters):
1162            # Counters should not be included
1163            if not param.polydisperse:
1164                continue
1165
1166            # Potential multishell params
1167            checked_list = ["Distribution of "+param.name, str(param.default),
1168                            str(param.limits[0]), str(param.limits[1]),
1169                            "35", "3", "gaussian"]
1170            FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
1171
1172            #TODO: Need to find cleaner way to input functions
1173            #func = QtGui.QComboBox()
1174            #func.addItems(['rectangle', 'array', 'lognormal', 'gaussian', 'schulz',])
1175            #func_index = self.lstPoly.model().index(row, 6)
1176            #self.lstPoly.setIndexWidget(func_index, func)
1177
1178        FittingUtilities.addPolyHeadersToModel(self._poly_model)
1179
1180    def setMagneticModel(self):
1181        """
1182        Set magnetism values on model
1183        """
1184        if not self.model_parameters:
1185            return
1186        self._magnet_model.clear()
1187        for param in self.model_parameters.call_parameters:
1188            if param.type != "magnetic":
1189                continue
1190            checked_list = [param.name,
1191                            str(param.default),
1192                            str(param.limits[0]),
1193                            str(param.limits[1]),
1194                            param.units]
1195            FittingUtilities.addCheckedListToModel(self._magnet_model, checked_list)
1196
1197        FittingUtilities.addHeadersToModel(self._magnet_model)
1198
1199    def enableStructureFactorControl(self, structure_factor):
1200        """
1201        Add structure factors to the list of parameters
1202        """
1203        if self.kernel_module.is_form_factor or structure_factor == 'None':
1204            self.enableStructureCombo()
1205        else:
1206            self.disableStructureCombo()
1207
1208    def addExtraShells(self):
1209        """
1210        Add a combobox for multiple shell display
1211        """
1212        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
1213
1214        if param_length == 0:
1215            return
1216
1217        # cell 1: variable name
1218        item1 = QtGui.QStandardItem(param_name)
1219
1220        func = QtGui.QComboBox()
1221        # Available range of shells displayed in the combobox
1222        func.addItems([str(i) for i in xrange(param_length+1)])
1223
1224        # Respond to index change
1225        func.currentIndexChanged.connect(self.modifyShellsInList)
1226
1227        # cell 2: combobox
1228        item2 = QtGui.QStandardItem()
1229        self._model_model.appendRow([item1, item2])
1230
1231        # Beautify the row:  span columns 2-4
1232        shell_row = self._model_model.rowCount()
1233        shell_index = self._model_model.index(shell_row-1, 1)
1234
1235        self.lstParams.setIndexWidget(shell_index, func)
1236        self._last_model_row = self._model_model.rowCount()
1237
1238        # Set the index to the state-kept value
1239        func.setCurrentIndex(self.current_shell_displayed
1240                             if self.current_shell_displayed < func.count() else 0)
1241
1242    def modifyShellsInList(self, index):
1243        """
1244        Add/remove additional multishell parameters
1245        """
1246        # Find row location of the combobox
1247        last_row = self._last_model_row
1248        remove_rows = self._model_model.rowCount() - last_row
1249
1250        if remove_rows > 1:
1251            self._model_model.removeRows(last_row, remove_rows)
1252
1253        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
1254        self.current_shell_displayed = index
1255
1256    def readFitPage(self, fp):
1257        """
1258        Read in state from a fitpage object and update GUI
1259        """
1260        assert isinstance(fp, FitPage)
1261        # Main tab info
1262        self.logic.data.filename = fp.filename
1263        self.data_is_loaded = fp.data_is_loaded
1264        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
1265        self.chkMagnetism.setCheckState(fp.is_magnetic)
1266        self.chk2DView.setCheckState(fp.is2D)
1267
1268        # Update the comboboxes
1269        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
1270        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
1271        if fp.current_factor:
1272            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
1273
1274        self.chi2 = fp.chi2
1275
1276        # Options tab
1277        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
1278        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
1279        self.npts = fp.fit_options[fp.NPTS]
1280        self.log_points = fp.fit_options[fp.LOG_POINTS]
1281        self.weighting = fp.fit_options[fp.WEIGHTING]
1282
1283        # Models
1284        self._model_model = fp.model_model
1285        self._poly_model = fp.poly_model
1286        self._magnet_model = fp.magnetism_model
1287
1288        # Resolution tab
1289        smearing = fp.smearing_options[fp.SMEARING_OPTION]
1290        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
1291        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
1292        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
1293        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
1294
1295        # TODO: add polidyspersity and magnetism
1296
1297    def saveToFitPage(self, fp):
1298        """
1299        Write current state to the given fitpage
1300        """
1301        assert isinstance(fp, FitPage)
1302
1303        # Main tab info
1304        fp.filename = self.logic.data.filename
1305        fp.data_is_loaded = self.data_is_loaded
1306        fp.is_polydisperse = self.chkPolydispersity.isChecked()
1307        fp.is_magnetic = self.chkMagnetism.isChecked()
1308        fp.is2D = self.chk2DView.isChecked()
1309        fp.data = self.data
1310
1311        # Use current models - they contain all the required parameters
1312        fp.model_model = self._model_model
1313        fp.poly_model = self._poly_model
1314        fp.magnetism_model = self._magnet_model
1315
1316        if self.cbCategory.currentIndex() != 0:
1317            fp.current_category = str(self.cbCategory.currentText())
1318            fp.current_model = str(self.cbModel.currentText())
1319
1320        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
1321            fp.current_factor = str(self.cbStructureFactor.currentText())
1322        else:
1323            fp.current_factor = ''
1324
1325        fp.chi2 = self.chi2
1326        fp.parameters_to_fit = self.parameters_to_fit
1327        fp.kernel_module = self.kernel_module
1328
1329        # Options tab
1330        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
1331        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
1332        fp.fit_options[fp.NPTS] = self.npts
1333        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
1334        fp.fit_options[fp.LOG_POINTS] = self.log_points
1335        fp.fit_options[fp.WEIGHTING] = self.weighting
1336
1337        # Resolution tab
1338        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
1339        fp.smearing_options[fp.SMEARING_OPTION] = smearing
1340        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
1341        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
1342        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
1343
1344        # TODO: add polidyspersity and magnetism
1345
1346
1347    def updateUndo(self):
1348        """
1349        Create a new state page and add it to the stack
1350        """
1351        if self.undo_supported:
1352            self.pushFitPage(self.currentState())
1353
1354    def currentState(self):
1355        """
1356        Return fit page with current state
1357        """
1358        new_page = FitPage()
1359        self.saveToFitPage(new_page)
1360
1361        return new_page
1362
1363    def pushFitPage(self, new_page):
1364        """
1365        Add a new fit page object with current state
1366        """
1367        self.page_stack.append(new_page)
1368
1369    def popFitPage(self):
1370        """
1371        Remove top fit page from stack
1372        """
1373        if self.page_stack:
1374            self.page_stack.pop()
1375
Note: See TracBrowser for help on using the repository browser.