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

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

Magnetic angles image widget

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