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
RevLine 
[60af928]1import json
[cd31251]2import os
[60af928]3from collections import defaultdict
[454670d]4from itertools import izip
[60af928]5
[5236449]6import logging
7import traceback
[cbcdd2c]8from twisted.internet import threads
[1bc27f1]9import numpy as np
[5236449]10
[60af928]11from PyQt4 import QtGui
12from PyQt4 import QtCore
[2add354]13from PyQt4 import QtWebKit
[60af928]14
15from sasmodels import generate
16from sasmodels import modelinfo
[5236449]17from sasmodels.sasview_model import load_standard_models
[f182f93]18from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
[5236449]19
[83eb5208]20import sas.qtgui.Utilities.GuiUtils as GuiUtils
[dc5ef15]21from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
22from sas.qtgui.Plotting.PlotterData import Data1D
23from sas.qtgui.Plotting.PlotterData import Data2D
[5236449]24
[1bc27f1]25from sas.qtgui.Perspectives.Fitting.UI.FittingWidgetUI import Ui_FittingWidgetUI
[dc5ef15]26from sas.qtgui.Perspectives.Fitting.FitThread import FitThread
27from sas.qtgui.Perspectives.Fitting.ModelThread import Calc1D
28from sas.qtgui.Perspectives.Fitting.ModelThread import Calc2D
[4d457df]29from sas.qtgui.Perspectives.Fitting.FittingLogic import FittingLogic
30from sas.qtgui.Perspectives.Fitting import FittingUtilities
[1bc27f1]31from sas.qtgui.Perspectives.Fitting.SmearingWidget import SmearingWidget
32from sas.qtgui.Perspectives.Fitting.OptionsWidget import OptionsWidget
33from sas.qtgui.Perspectives.Fitting.FitPage import FitPage
[ad6b4e2]34from sas.qtgui.Perspectives.Fitting.ViewDelegate import ModelViewDelegate
[6011788]35from sas.qtgui.Perspectives.Fitting.ViewDelegate import PolyViewDelegate
[60af928]36
37TAB_MAGNETISM = 4
38TAB_POLY = 3
[cbcdd2c]39CATEGORY_DEFAULT = "Choose category..."
[4d457df]40CATEGORY_STRUCTURE = "Structure Factor"
[351b53e]41STRUCTURE_DEFAULT = "None"
[60af928]42
43class FittingWidget(QtGui.QWidget, Ui_FittingWidgetUI):
44    """
[f46f6dc]45    Main widget for selecting form and structure factor models
[60af928]46    """
[1bc27f1]47    def __init__(self, parent=None, data=None, tab_id=1):
[60af928]48
49        super(FittingWidget, self).__init__()
50
[86f88d1]51        # Necessary globals
[cbcdd2c]52        self.parent = parent
[2a432e7]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        """
[cbcdd2c]129        # SasModel is loaded
[60af928]130        self.model_is_loaded = False
[cbcdd2c]131        # Data[12]D passed and set
[5236449]132        self.data_is_loaded = False
[cbcdd2c]133        # Current SasModel in view
[5236449]134        self.kernel_module = None
[cbcdd2c]135        # Current SasModel view dimension
[60af928]136        self.is2D = False
[cbcdd2c]137        # Current SasModel is multishell
[86f88d1]138        self.model_has_shells = False
[cbcdd2c]139        # Utility variable to enable unselectable option in category combobox
[86f88d1]140        self._previous_category_index = 0
[cbcdd2c]141        # Utility variable for multishell display
[86f88d1]142        self._last_model_row = 0
[cbcdd2c]143        # Dictionary of {model name: model class} for the current category
[5236449]144        self.models = {}
[f182f93]145        # Parameters to fit
146        self.parameters_to_fit = None
[180bd54]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
[2add354]153        self.chi2 = None
[6011788]154        # Does the control support UNDO/REDO
155        # temporarily off
[2241130]156        self.undo_supported = False
157        self.page_stack = []
[6011788]158
[d48cc19]159        # Data for chosen model
160        self.model_data = None
161
[a9b568c]162        # Which shell is being currently displayed?
163        self.current_shell_displayed = 0
[f182f93]164        self.has_error_column = False
[a9b568c]165
[2a432e7]166        # signal communicator
[cbcdd2c]167        self.communicate = self.parent.communicate
[60af928]168
[2a432e7]169    def initializeWidgets(self):
170        """
171        Initialize widgets for tabs
172        """
[180bd54]173        # Options widget
174        layout = QtGui.QGridLayout()
175        self.options_widget = OptionsWidget(self, self.logic)
[1bc27f1]176        layout.addWidget(self.options_widget)
[180bd54]177        self.tabOptions.setLayout(layout)
178
[e1e3e09]179        # Smearing widget
180        layout = QtGui.QGridLayout()
181        self.smearing_widget = SmearingWidget(self)
[1bc27f1]182        layout.addWidget(self.smearing_widget)
[180bd54]183        self.tabResolution.setLayout(layout)
[e1e3e09]184
[b1e36a3]185        # Define bold font for use in various controls
[1bc27f1]186        self.boldFont = QtGui.QFont()
[a0f5c36]187        self.boldFont.setBold(True)
188
189        # Set data label
[b1e36a3]190        self.label.setFont(self.boldFont)
191        self.label.setText("No data loaded")
192        self.lblFilename.setText("")
193
[2a432e7]194    def initializeModels(self):
195        """
196        Set up models and views
197        """
[86f88d1]198        # Set the main models
[cd31251]199        # We can't use a single model here, due to restrictions on flattening
200        # the model tree with subclassed QAbstractProxyModel...
[60af928]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)
[5236449]207        self.readCategoryInfo()
[60af928]208        self.model_parameters = None
[ad6b4e2]209
210        # Delegates for custom editing and display
211        self.lstParams.setItemDelegate(ModelViewDelegate(self))
212
[86f88d1]213        self.lstParams.setAlternatingRowColors(True)
[61a92d4]214        stylesheet = """
[2a432e7]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;
[61a92d4]218            }
[2a432e7]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           """
[61a92d4]232        self.lstParams.setStyleSheet(stylesheet)
[672b8ab]233        self.lstParams.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
[2add354]234        self.lstParams.customContextMenuRequested.connect(self.showModelDescription)
[60af928]235
236        # Poly model displayed in poly list
[811bec1]237        self.lstPoly.setModel(self._poly_model)
[60af928]238        self.setPolyModel()
239        self.setTableProperties(self.lstPoly)
[6011788]240        # Delegates for custom editing and display
241        self.lstPoly.setItemDelegate(PolyViewDelegate(self))
[60af928]242
243        # Magnetism model displayed in magnetism list
244        self.lstMagnetic.setModel(self._magnet_model)
245        self.setMagneticModel()
246        self.setTableProperties(self.lstMagnetic)
247
[2a432e7]248    def initializeCategoryCombo(self):
249        """
250        Model category combo setup
251        """
[60af928]252        category_list = sorted(self.master_category_dict.keys())
[86f88d1]253        self.cbCategory.addItem(CATEGORY_DEFAULT)
[60af928]254        self.cbCategory.addItems(category_list)
[4d457df]255        self.cbCategory.addItem(CATEGORY_STRUCTURE)
[6f7f652]256        self.cbCategory.setCurrentIndex(0)
[60af928]257
[e1e3e09]258    def setEnablementOnDataLoad(self):
259        """
260        Enable/disable various UI elements based on data loaded
261        """
[cbcdd2c]262        # Tag along functionality
[b1e36a3]263        self.label.setText("Data loaded from: ")
[a0f5c36]264        self.lblFilename.setText(self.logic.data.filename)
[5236449]265        self.updateQRange()
[e1e3e09]266        # Switch off Data2D control
267        self.chk2DView.setEnabled(False)
268        self.chk2DView.setVisible(False)
269        self.chkMagnetism.setEnabled(True)
[180bd54]270        # Similarly on other tabs
271        self.options_widget.setEnablementOnDataLoad()
[e1e3e09]272
273        # Smearing tab
274        self.smearing_widget.updateSmearing(self.data)
[60af928]275
[f46f6dc]276    def acceptsData(self):
277        """ Tells the caller this widget can accept new dataset """
[5236449]278        return not self.data_is_loaded
[f46f6dc]279
[6f7f652]280    def disableModelCombo(self):
[cbcdd2c]281        """ Disable the combobox """
[6f7f652]282        self.cbModel.setEnabled(False)
[b1e36a3]283        self.lblModel.setEnabled(False)
[6f7f652]284
285    def enableModelCombo(self):
[cbcdd2c]286        """ Enable the combobox """
[6f7f652]287        self.cbModel.setEnabled(True)
[b1e36a3]288        self.lblModel.setEnabled(True)
[6f7f652]289
290    def disableStructureCombo(self):
[cbcdd2c]291        """ Disable the combobox """
[6f7f652]292        self.cbStructureFactor.setEnabled(False)
[b1e36a3]293        self.lblStructure.setEnabled(False)
[6f7f652]294
295    def enableStructureCombo(self):
[cbcdd2c]296        """ Enable the combobox """
[6f7f652]297        self.cbStructureFactor.setEnabled(True)
[b1e36a3]298        self.lblStructure.setEnabled(True)
[6f7f652]299
[0268aed]300    def togglePoly(self, isChecked):
[454670d]301        """ Enable/disable the polydispersity tab """
[0268aed]302        self.tabFitting.setTabEnabled(TAB_POLY, isChecked)
303
304    def toggleMagnetism(self, isChecked):
[454670d]305        """ Enable/disable the magnetism tab """
[0268aed]306        self.tabFitting.setTabEnabled(TAB_MAGNETISM, isChecked)
307
308    def toggle2D(self, isChecked):
[454670d]309        """ Enable/disable the controls dependent on 1D/2D data instance """
[0268aed]310        self.chkMagnetism.setEnabled(isChecked)
311        self.is2D = isChecked
[1970780]312        # Reload the current model
[e1e3e09]313        if self.kernel_module:
314            self.onSelectModel()
[5236449]315
[86f88d1]316    def initializeControls(self):
317        """
318        Set initial control enablement
319        """
320        self.cmdFit.setEnabled(False)
[d48cc19]321        self.cmdPlot.setEnabled(False)
[180bd54]322        self.options_widget.cmdComputePoints.setVisible(False) # probably redundant
[86f88d1]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)
[cbcdd2c]329        # Tabs
[86f88d1]330        self.tabFitting.setTabEnabled(TAB_POLY, False)
331        self.tabFitting.setTabEnabled(TAB_MAGNETISM, False)
332        self.lblChi2Value.setText("---")
[e1e3e09]333        # Smearing tab
334        self.smearing_widget.updateSmearing(self.data)
[180bd54]335        # Line edits in the option tab
336        self.updateQRange()
[86f88d1]337
338    def initializeSignals(self):
339        """
340        Connect GUI element signals
341        """
[cbcdd2c]342        # Comboboxes
[cd31251]343        self.cbStructureFactor.currentIndexChanged.connect(self.onSelectStructureFactor)
344        self.cbCategory.currentIndexChanged.connect(self.onSelectCategory)
345        self.cbModel.currentIndexChanged.connect(self.onSelectModel)
[cbcdd2c]346        # Checkboxes
[86f88d1]347        self.chk2DView.toggled.connect(self.toggle2D)
348        self.chkPolydispersity.toggled.connect(self.togglePoly)
349        self.chkMagnetism.toggled.connect(self.toggleMagnetism)
[cbcdd2c]350        # Buttons
[5236449]351        self.cmdFit.clicked.connect(self.onFit)
[cbcdd2c]352        self.cmdPlot.clicked.connect(self.onPlot)
[2add354]353        self.cmdHelp.clicked.connect(self.onHelp)
[cbcdd2c]354
355        # Respond to change in parameters from the UI
356        self._model_model.itemChanged.connect(self.updateParamsFromModel)
[cd31251]357        self._poly_model.itemChanged.connect(self.onPolyModelChange)
358        # TODO after the poly_model prototype accepted
359        #self._magnet_model.itemChanged.connect(self.onMagneticModelChange)
[86f88d1]360
[180bd54]361        # Signals from separate tabs asking for replot
362        self.options_widget.plot_signal.connect(self.onOptionsUpdate)
363
[2add354]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()
[672b8ab]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))
[2add354]383
[0268aed]384    def onSelectModel(self):
[cbcdd2c]385        """
[0268aed]386        Respond to select Model from list event
[cbcdd2c]387        """
[0268aed]388        model = str(self.cbModel.currentText())
389
390        # Reset structure factor
391        self.cbStructureFactor.setCurrentIndex(0)
392
[f182f93]393        # Reset parameters to fit
394        self.parameters_to_fit = None
[d7ff531]395        self.has_error_column = False
[f182f93]396
[fd1ae6d1]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):
[d48cc19]411        # Set enablement on calculate/plot
412        self.cmdPlot.setEnabled(True)
413
[fd1ae6d1]414        # kernel parameters -> model_model
415        self.SASModelToQModel(model, structure_factor)
[0268aed]416
417        if self.data_is_loaded:
[d48cc19]418            self.cmdPlot.setText("Show Plot")
[0268aed]419            self.calculateQGridForModel()
420        else:
[d48cc19]421            self.cmdPlot.setText("Calculate")
[0268aed]422            # Create default datasets if no data passed
423            self.createDefaultDataset()
424
[6011788]425        # Update state stack
[00b3b40]426        self.updateUndo()
[2add354]427
[cd31251]428    def onSelectCategory(self):
[60af928]429        """
430        Select Category from list
431        """
[4d457df]432        category = str(self.cbCategory.currentText())
[86f88d1]433        # Check if the user chose "Choose category entry"
[4d457df]434        if category == CATEGORY_DEFAULT:
[86f88d1]435            # if the previous category was not the default, keep it.
436            # Otherwise, just return
437            if self._previous_category_index != 0:
[351b53e]438                # We need to block signals, or else state changes on perceived unchanged conditions
439                self.cbCategory.blockSignals(True)
[86f88d1]440                self.cbCategory.setCurrentIndex(self._previous_category_index)
[351b53e]441                self.cbCategory.blockSignals(False)
[86f88d1]442            return
443
[4d457df]444        if category == CATEGORY_STRUCTURE:
[6f7f652]445            self.disableModelCombo()
446            self.enableStructureCombo()
[29eb947]447            self._model_model.clear()
[6f7f652]448            return
449
[cbcdd2c]450        # Safely clear and enable the model combo
[6f7f652]451        self.cbModel.blockSignals(True)
452        self.cbModel.clear()
453        self.cbModel.blockSignals(False)
454        self.enableModelCombo()
455        self.disableStructureCombo()
456
[86f88d1]457        self._previous_category_index = self.cbCategory.currentIndex()
[cbcdd2c]458        # Retrieve the list of models
[4d457df]459        model_list = self.master_category_dict[category]
[cbcdd2c]460        # Populate the models combobox
[b1e36a3]461        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
[4d457df]462
[0268aed]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
[00b3b40]472        # TODO: abstract away hardcoded column numbers
[0268aed]473        if model_column == 0:
[00b3b40]474            # Is the parameter checked for fitting?
[0268aed]475            value = item.checkState()
[00b3b40]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
[0268aed]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
[2add354]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
[0268aed]511    def onFit(self):
512        """
513        Perform fitting on the current data
514        """
[f182f93]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
[180bd54]524        # Potential weights added directly to data
[9d266d2]525        self.addWeightingToData(data)
526
[98b13f72]527        # Potential smearing added
[180bd54]528        # Remember that smearing_min/max can be None ->
529        # deal with it until Python gets discriminated unions
[98b13f72]530        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
531
[f182f93]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)
[2add354]546
[f182f93]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)
[02ddfb4]565        calc_thread.addErrback(self.fitFailed)
[f182f93]566
567        #disable the Fit button
[d7ff531]568        self.cmdFit.setText('Calculating...')
569        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
[f182f93]570        self.cmdFit.setEnabled(False)
[0268aed]571
[f182f93]572    def updateFit(self):
573        """
574        """
575        print "UPDATE FIT"
[0268aed]576        pass
577
[02ddfb4]578    def fitFailed(self, reason):
579        """
580        """
581        print "FIT FAILED: ", reason
582        pass
583
[f182f93]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)
[d7ff531]592
593        assert result is not None
594
[f182f93]595        res_list = result[0]
596        res = res_list[0]
597        if res.fitness is None or \
[180bd54]598            not np.isfinite(res.fitness) or \
[1bc27f1]599            np.any(res.pvec is None) or \
[180bd54]600            not np.all(np.isfinite(res.pvec)):
[f182f93]601            msg = "Fitting did not converge!!!"
[454670d]602            self.communicate.statusBarUpdateSignal.emit(msg)
[f182f93]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
[2add354]610        self.chi2 = res.fitness
[f182f93]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
[d7ff531]621        # update charts
622        self.onPlot()
623
[f182f93]624        # Read only value - we can get away by just printing it here
[2add354]625        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
[f182f93]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
[d7ff531]644        def updateFittedValues(row_i):
[f182f93]645            # Utility function for main model update
[d7ff531]646            # internal so can use closure for param_dict
[f182f93]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
[454670d]651            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
652            self._model_model.item(row_i, 1).setText(param_repr)
[f182f93]653            if self.has_error_column:
[454670d]654                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
655                self._model_model.item(row_i, 2).setText(error_repr)
[f182f93]656
[d7ff531]657        def createErrorColumn(row_i):
[f182f93]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
[2da5759]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)
[d7ff531]670        self.iterateOverModel(updateFittedValues)
[2da5759]671        self._model_model.blockSignals(False)
[f182f93]672
673        if self.has_error_column:
674            return
675
676        error_column = []
[d7ff531]677        self.iterateOverModel(createErrorColumn)
[f182f93]678
[d7ff531]679        # switch off reponse to model change
680        self._model_model.blockSignals(True)
[f182f93]681        self._model_model.insertColumn(2, error_column)
[d7ff531]682        self._model_model.blockSignals(False)
[f182f93]683        FittingUtilities.addErrorHeadersToModel(self._model_model)
[d7ff531]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
[f182f93]692
[0268aed]693    def onPlot(self):
694        """
695        Plot the current set of data
696        """
[d48cc19]697        # Regardless of previous state, this should now be `plot show` functionality only
698        self.cmdPlot.setText("Show Plot")
[1bc27f1]699        if not self.data_is_loaded:
700            self.recalculatePlotData()
[d48cc19]701        self.showPlot()
702
703    def recalculatePlotData(self):
704        """
705        Generate a new dataset for model
706        """
[180bd54]707        if not self.data_is_loaded:
[0268aed]708            self.createDefaultDataset()
709        self.calculateQGridForModel()
710
[d48cc19]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
[180bd54]720    def onOptionsUpdate(self):
[0268aed]721        """
[180bd54]722        Update local option values and replot
[0268aed]723        """
[180bd54]724        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
725            self.options_widget.state()
[61a92d4]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))
[d48cc19]729        self.recalculatePlotData()
[6c8fb2c]730
[0268aed]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
[4d457df]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:
[180bd54]747            qmax = self.q_range_max/np.sqrt(2)
[4d457df]748            qstep = self.npts
749            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
[180bd54]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)
[1bc27f1]753            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
[180bd54]754            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
[4d457df]755        else:
[180bd54]756            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
[1bc27f1]757                                   num=self.npts, endpoint=True)
[180bd54]758        self.logic.createDefault1dData(interval, self.tab_id)
[60af928]759
[5236449]760    def readCategoryInfo(self):
[60af928]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
[cbcdd2c]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:
[60af928]772            self.master_category_dict = json.load(cat_file)
[5236449]773            self.regenerateModelDict()
[60af928]774
[5236449]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):
[60af928]781        """
[cbcdd2c]782        Regenerates self.by_model_dict which has each model name as the
[60af928]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
[86f88d1]792    def addBackgroundToModel(self, model):
793        """
794        Adds background parameter with default values to the model
795        """
[cbcdd2c]796        assert isinstance(model, QtGui.QStandardItemModel)
[86f88d1]797        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
[4d457df]798        FittingUtilities.addCheckedListToModel(model, checked_list)
[2add354]799        last_row = model.rowCount()-1
800        model.item(last_row, 0).setEditable(False)
801        model.item(last_row, 4).setEditable(False)
[86f88d1]802
803    def addScaleToModel(self, model):
804        """
805        Adds scale parameter with default values to the model
806        """
[cbcdd2c]807        assert isinstance(model, QtGui.QStandardItemModel)
[86f88d1]808        checked_list = ['scale', '1.0', '0.0', 'inf', '']
[4d457df]809        FittingUtilities.addCheckedListToModel(model, checked_list)
[2add354]810        last_row = model.rowCount()-1
811        model.item(last_row, 0).setEditable(False)
812        model.item(last_row, 4).setEditable(False)
[86f88d1]813
[9d266d2]814    def addWeightingToData(self, data):
815        """
816        Adds weighting contribution to fitting data
[1bc27f1]817        """
[e1e3e09]818        # Send original data for weighting
[dc5ef15]819        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
[180bd54]820        update_module = data.err_data if self.is2D else data.dy
[6964d44]821        # Overwrite relevant values in data
[180bd54]822        update_module = weight
[9d266d2]823
[0268aed]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
[180bd54]834        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
[0268aed]835
[4d457df]836    def SASModelToQModel(self, model_name, structure_factor=None):
[60af928]837        """
[cbcdd2c]838        Setting model parameters into table based on selected category
[60af928]839        """
840        # Crete/overwrite model items
841        self._model_model.clear()
[5236449]842
[fd1ae6d1]843        # First, add parameters from the main model
844        if model_name is not None:
845            self.fromModelToQModel(model_name)
[5236449]846
[fd1ae6d1]847        # Then, add structure factor derived parameters
[cd31251]848        if structure_factor is not None and structure_factor != "None":
[fd1ae6d1]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)
[cd31251]853        else:
[fd1ae6d1]854            # Allow the SF combobox visibility for the given sasmodel
855            self.enableStructureFactorControl(structure_factor)
[cd31251]856
[fd1ae6d1]857        # Then, add multishells
858        if model_name is not None:
859            # Multishell models need additional treatment
860            self.addExtraShells()
[86f88d1]861
[5236449]862        # Add polydispersity to the model
[86f88d1]863        self.setPolyModel()
[5236449]864        # Add magnetic parameters to the model
[86f88d1]865        self.setMagneticModel()
[5236449]866
[a9b568c]867        # Adjust the table cells width
868        self.lstParams.resizeColumnToContents(0)
869        self.lstParams.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
870
[5236449]871        # Now we claim the model has been loaded
[86f88d1]872        self.model_is_loaded = True
873
[fd1ae6d1]874        # (Re)-create headers
875        FittingUtilities.addHeadersToModel(self._model_model)
[6964d44]876        self.lstParams.header().setFont(self.boldFont)
[fd1ae6d1]877
[5236449]878        # Update Q Ranges
879        self.updateQRange()
880
[fd1ae6d1]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
[6964d44]892        temp_undo_state = self.undo_supported
893        self.undo_supported = False
[fd1ae6d1]894        self.addScaleToModel(self._model_model)
895        self.addBackgroundToModel(self._model_model)
[6964d44]896        self.undo_supported = temp_undo_state
[fd1ae6d1]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
[cd31251]918    def updateParamsFromModel(self, item):
919        """
920        Callback method for updating the sasmodel parameters with the GUI values
921        """
[cbcdd2c]922        model_column = item.column()
[cd31251]923
924        if model_column == 0:
[f182f93]925            self.checkboxSelected(item)
[2add354]926            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
[6964d44]927            # Update state stack
928            self.updateUndo()
[cd31251]929            return
930
[f182f93]931        model_row = item.row()
932        name_index = self._model_model.index(model_row, 0)
933
[cd31251]934        # Extract changed value. Assumes proper validation by QValidator/Delegate
[2add354]935        try:
936            value = float(item.text())
937        except ValueError:
938            # Unparsable field
939            return
[cbcdd2c]940        parameter_name = str(self._model_model.data(name_index).toPyObject()) # sld, background etc.
[00b3b40]941        property_index = self._model_model.headerData(1, model_column).toInt()[0]-1 # Value, min, max, etc.
[cbcdd2c]942
[00b3b40]943        # Update the parameter value - note: this supports +/-inf as well
[cbcdd2c]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]
[00b3b40]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
[cbcdd2c]951
[d7ff531]952        # Force the chart update when actual parameters changed
953        if model_column == 1:
[d48cc19]954            self.recalculatePlotData()
[7d077d1]955
[2241130]956        # Update state stack
[00b3b40]957        self.updateUndo()
[2241130]958
[f182f93]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
[1bc27f1]977        _ = [self._model_model.item(row, 0).setCheckState(status) for row in rows]
[f182f93]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
[6fd4e36]985    def nameForFittedData(self, name):
[5236449]986        """
[6fd4e36]987        Generate name for the current fit
[5236449]988        """
989        if self.is2D:
990            name += "2d"
991        name = "M%i [%s]" % (self.tab_id, name)
[6fd4e36]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:
[0268aed]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
[7d077d1]1004                fitted_data.symbol = "Line"
[6fd4e36]1005            self.updateModelIndex(fitted_data)
1006        else:
[0268aed]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"
[6fd4e36]1012            self.createTheoryIndex(fitted_data)
1013
1014    def updateModelIndex(self, fitted_data):
1015        """
1016        Update a QStandardModelIndex containing model data
1017        """
[00b3b40]1018        name = self.nameFromData(fitted_data)
[0268aed]1019        # Make this a line if no other defined
[7d077d1]1020        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
[0268aed]1021            fitted_data.symbol = 'Line'
[6fd4e36]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        """
[00b3b40]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        """
[0268aed]1038        if fitted_data.name is None:
[00b3b40]1039            name = self.nameForFittedData(self.logic.data.filename)
[0268aed]1040            fitted_data.title = name
1041            fitted_data.name = name
1042            fitted_data.filename = name
1043        else:
1044            name = fitted_data.name
[00b3b40]1045        return name
[5236449]1046
[4d457df]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
[b1e36a3]1055    def calculateQGridForModel(self):
[86f88d1]1056        """
1057        Prepare the fitting data object, based on current ModelModel
1058        """
[180bd54]1059        if self.kernel_module is None:
1060            return
[4d457df]1061        # Awful API to a backend method.
1062        method = self.methodCalculateForData()(data=self.data,
[1bc27f1]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)
[4d457df]1076
1077        calc_thread = threads.deferToThread(method.compute)
1078        calc_thread.addCallback(self.methodCompleteForData())
[6964d44]1079        calc_thread.addErrback(self.calculateDataFailed())
1080
1081    def calculateDataFailed(self):
1082        """
1083        """
1084        print "Calculate Data failed."
[5236449]1085
[cbcdd2c]1086    def complete1D(self, return_data):
[5236449]1087        """
[4d457df]1088        Plot the current 1D data
1089        """
[d48cc19]1090        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
1091        self.calculateResiduals(fitted_data)
1092        self.model_data = fitted_data
[cbcdd2c]1093
1094    def complete2D(self, return_data):
1095        """
[4d457df]1096        Plot the current 2D data
1097        """
[6fd4e36]1098        fitted_data = self.logic.new2DPlot(return_data)
1099        self.calculateResiduals(fitted_data)
[d48cc19]1100        self.model_data = fitted_data
[6fd4e36]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
[7d077d1]1107        fitted_data.symbol = "Line"
[6964d44]1108
1109        # Modify fitted_data with weighting
1110        self.addWeightingToData(fitted_data)
1111
[6fd4e36]1112        self.createNewIndex(fitted_data)
1113        # Calculate difference between return_data and logic.data
[2add354]1114        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
[6fd4e36]1115        # Update the control
[2add354]1116        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
[f182f93]1117        self.lblChi2Value.setText(chi2_repr)
[cbcdd2c]1118
[d48cc19]1119        self.communicate.plotUpdateSignal.emit([fitted_data])
1120
[0268aed]1121        # Plot residuals if actual data
1122        if self.data_is_loaded:
1123            residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
[7d077d1]1124            residuals_plot.id = "Residual " + residuals_plot.id
[0268aed]1125            self.createNewIndex(residuals_plot)
[9f25bce]1126            self.communicate.plotUpdateSignal.emit([residuals_plot])
[5236449]1127
1128    def calcException(self, etype, value, tb):
1129        """
[b1e36a3]1130        Something horrible happened in the deferred.
[5236449]1131        """
1132        logging.error("".join(traceback.format_exception(etype, value, tb)))
[60af928]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)
[f46f6dc]1143        table.resizeColumnsToContents()
1144
[60af928]1145        # Header
1146        header = table.horizontalHeader()
[f46f6dc]1147        header.setResizeMode(QtGui.QHeaderView.ResizeToContents)
1148
1149        header.ResizeMode(QtGui.QHeaderView.Interactive)
[b1e36a3]1150        # Resize column 0 and 6 to content
[f46f6dc]1151        header.setResizeMode(0, QtGui.QHeaderView.ResizeToContents)
1152        header.setResizeMode(6, QtGui.QHeaderView.ResizeToContents)
[60af928]1153
1154    def setPolyModel(self):
1155        """
1156        Set polydispersity values
1157        """
[86f88d1]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),
[cbcdd2c]1168                            str(param.limits[0]), str(param.limits[1]),
[6011788]1169                            "35", "3", "gaussian"]
[4d457df]1170            FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
[86f88d1]1171
1172            #TODO: Need to find cleaner way to input functions
[6011788]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)
[86f88d1]1177
[4d457df]1178        FittingUtilities.addPolyHeadersToModel(self._poly_model)
[60af928]1179
1180    def setMagneticModel(self):
1181        """
1182        Set magnetism values on model
1183        """
[86f88d1]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]
[4d457df]1195            FittingUtilities.addCheckedListToModel(self._magnet_model, checked_list)
[86f88d1]1196
[4d457df]1197        FittingUtilities.addHeadersToModel(self._magnet_model)
[60af928]1198
[fd1ae6d1]1199    def enableStructureFactorControl(self, structure_factor):
[cd31251]1200        """
1201        Add structure factors to the list of parameters
1202        """
[fd1ae6d1]1203        if self.kernel_module.is_form_factor or structure_factor == 'None':
[cd31251]1204            self.enableStructureCombo()
1205        else:
1206            self.disableStructureCombo()
1207
[60af928]1208    def addExtraShells(self):
1209        """
[f46f6dc]1210        Add a combobox for multiple shell display
[60af928]1211        """
[4d457df]1212        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
[f46f6dc]1213
1214        if param_length == 0:
1215            return
1216
[6f7f652]1217        # cell 1: variable name
[f46f6dc]1218        item1 = QtGui.QStandardItem(param_name)
1219
[60af928]1220        func = QtGui.QComboBox()
[b1e36a3]1221        # Available range of shells displayed in the combobox
1222        func.addItems([str(i) for i in xrange(param_length+1)])
[a9b568c]1223
[b1e36a3]1224        # Respond to index change
[86f88d1]1225        func.currentIndexChanged.connect(self.modifyShellsInList)
[60af928]1226
[6f7f652]1227        # cell 2: combobox
[f46f6dc]1228        item2 = QtGui.QStandardItem()
1229        self._model_model.appendRow([item1, item2])
[60af928]1230
[6f7f652]1231        # Beautify the row:  span columns 2-4
[60af928]1232        shell_row = self._model_model.rowCount()
[f46f6dc]1233        shell_index = self._model_model.index(shell_row-1, 1)
[86f88d1]1234
[4d457df]1235        self.lstParams.setIndexWidget(shell_index, func)
[86f88d1]1236        self._last_model_row = self._model_model.rowCount()
1237
[a9b568c]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
[86f88d1]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
[4d457df]1253        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
[a9b568c]1254        self.current_shell_displayed = index
[60af928]1255
[672b8ab]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
[d60da0c]1284        self._model_model = fp.model_model
1285        self._poly_model = fp.poly_model
1286        self._magnet_model = fp.magnetism_model
[672b8ab]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
[6964d44]1327        fp.kernel_module = self.kernel_module
[672b8ab]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
[00b3b40]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
[672b8ab]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        """
[6011788]1367        self.page_stack.append(new_page)
[672b8ab]1368
1369    def popFitPage(self):
1370        """
1371        Remove top fit page from stack
1372        """
[6011788]1373        if self.page_stack:
1374            self.page_stack.pop()
[672b8ab]1375
Note: See TracBrowser for help on using the repository browser.