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

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 56b22f9 was 02ddfb4, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Unit tests for GUI fitting: SASVIEW-507

  • Property mode set to 100644
File size: 39.2 KB
RevLine 
[60af928]1import sys
2import json
[cd31251]3import os
[180bd54]4import numpy as np
[60af928]5from collections import defaultdict
[454670d]6from itertools import izip
[60af928]7
[5236449]8import logging
9import traceback
[cbcdd2c]10from twisted.internet import threads
[5236449]11
[60af928]12from PyQt4 import QtGui
13from PyQt4 import QtCore
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
19from sas.sasgui.perspectives.fitting.fit_thread import FitThread
[5236449]20
[60af928]21from sas.sasgui.guiframe.CategoryInstaller import CategoryInstaller
[5236449]22from sas.sasgui.guiframe.dataFitting import Data1D
23from sas.sasgui.guiframe.dataFitting import Data2D
[83eb5208]24import sas.qtgui.Utilities.GuiUtils as GuiUtils
[5236449]25from sas.sasgui.perspectives.fitting.model_thread import Calc1D
[cbcdd2c]26from sas.sasgui.perspectives.fitting.model_thread import Calc2D
[e1e3e09]27from sas.sasgui.perspectives.fitting.utils import get_weight
[5236449]28
29from UI.FittingWidgetUI import Ui_FittingWidgetUI
[4d457df]30from sas.qtgui.Perspectives.Fitting.FittingLogic import FittingLogic
31from sas.qtgui.Perspectives.Fitting import FittingUtilities
[e1e3e09]32from SmearingWidget import SmearingWidget
[180bd54]33from OptionsWidget import OptionsWidget
[60af928]34
35TAB_MAGNETISM = 4
36TAB_POLY = 3
[cbcdd2c]37CATEGORY_DEFAULT = "Choose category..."
[4d457df]38CATEGORY_STRUCTURE = "Structure Factor"
[351b53e]39STRUCTURE_DEFAULT = "None"
[60af928]40
41class FittingWidget(QtGui.QWidget, Ui_FittingWidgetUI):
42    """
[f46f6dc]43    Main widget for selecting form and structure factor models
[60af928]44    """
[811bec1]45    def __init__(self, parent=None, data=None, id=1):
[60af928]46
47        super(FittingWidget, self).__init__()
48
[86f88d1]49        # Necessary globals
[cbcdd2c]50        self.parent = parent
51        # SasModel is loaded
[60af928]52        self.model_is_loaded = False
[cbcdd2c]53        # Data[12]D passed and set
[5236449]54        self.data_is_loaded = False
[cbcdd2c]55        # Current SasModel in view
[5236449]56        self.kernel_module = None
[cbcdd2c]57        # Current SasModel view dimension
[60af928]58        self.is2D = False
[cbcdd2c]59        # Current SasModel is multishell
[86f88d1]60        self.model_has_shells = False
[cbcdd2c]61        # Utility variable to enable unselectable option in category combobox
[86f88d1]62        self._previous_category_index = 0
[cbcdd2c]63        # Utility variable for multishell display
[86f88d1]64        self._last_model_row = 0
[cbcdd2c]65        # Dictionary of {model name: model class} for the current category
[5236449]66        self.models = {}
[f182f93]67        # Parameters to fit
68        self.parameters_to_fit = None
[180bd54]69        # Fit options
70        self.q_range_min = 0.005
71        self.q_range_max = 0.1
72        self.npts = 25
73        self.log_points = False
74        self.weighting = 0
[5236449]75
[d48cc19]76        # Data for chosen model
77        self.model_data = None
78
[5236449]79        # Which tab is this widget displayed in?
80        self.tab_id = id
81
[a9b568c]82        # Which shell is being currently displayed?
83        self.current_shell_displayed = 0
[f182f93]84        self.has_error_column = False
[a9b568c]85
[cbcdd2c]86        # Main Data[12]D holder
[4d457df]87        self.logic = FittingLogic(data=data)
[60af928]88
[86f88d1]89        # Main GUI setup up
[60af928]90        self.setupUi(self)
91        self.setWindowTitle("Fitting")
[cbcdd2c]92        self.communicate = self.parent.communicate
[60af928]93
[180bd54]94        # Options widget
95        layout = QtGui.QGridLayout()
96        self.options_widget = OptionsWidget(self, self.logic)
97        layout.addWidget(self.options_widget) 
98        self.tabOptions.setLayout(layout)
99
[e1e3e09]100        # Smearing widget
101        layout = QtGui.QGridLayout()
102        self.smearing_widget = SmearingWidget(self)
103        layout.addWidget(self.smearing_widget) 
[180bd54]104        self.tabResolution.setLayout(layout)
[e1e3e09]105
[b1e36a3]106        # Define bold font for use in various controls
[a0f5c36]107        self.boldFont=QtGui.QFont()
108        self.boldFont.setBold(True)
109
110        # Set data label
[b1e36a3]111        self.label.setFont(self.boldFont)
112        self.label.setText("No data loaded")
113        self.lblFilename.setText("")
114
[86f88d1]115        # Set the main models
[cd31251]116        # We can't use a single model here, due to restrictions on flattening
117        # the model tree with subclassed QAbstractProxyModel...
[60af928]118        self._model_model = QtGui.QStandardItemModel()
119        self._poly_model = QtGui.QStandardItemModel()
120        self._magnet_model = QtGui.QStandardItemModel()
121
122        # Param model displayed in param list
123        self.lstParams.setModel(self._model_model)
[5236449]124        self.readCategoryInfo()
[60af928]125        self.model_parameters = None
[86f88d1]126        self.lstParams.setAlternatingRowColors(True)
[61a92d4]127        stylesheet = """
128            QTreeView{
129                alternate-background-color: #f6fafb;
130                background: #e8f4fc;
131            }
132        """
133        self.lstParams.setStyleSheet(stylesheet)
[60af928]134
135        # Poly model displayed in poly list
[811bec1]136        self.lstPoly.setModel(self._poly_model)
[60af928]137        self.setPolyModel()
138        self.setTableProperties(self.lstPoly)
139
140        # Magnetism model displayed in magnetism list
141        self.lstMagnetic.setModel(self._magnet_model)
142        self.setMagneticModel()
143        self.setTableProperties(self.lstMagnetic)
144
[5236449]145        # Defaults for the structure factors
[6f7f652]146        self.setDefaultStructureCombo()
147
[5236449]148        # Make structure factor and model CBs disabled
[6f7f652]149        self.disableModelCombo()
150        self.disableStructureCombo()
[60af928]151
[6f7f652]152        # Generate the category list for display
[60af928]153        category_list = sorted(self.master_category_dict.keys())
[86f88d1]154        self.cbCategory.addItem(CATEGORY_DEFAULT)
[60af928]155        self.cbCategory.addItems(category_list)
[4d457df]156        self.cbCategory.addItem(CATEGORY_STRUCTURE)
[6f7f652]157        self.cbCategory.setCurrentIndex(0)
[60af928]158
[86f88d1]159        # Connect signals to controls
160        self.initializeSignals()
[60af928]161
[86f88d1]162        # Initial control state
163        self.initializeControls()
[60af928]164
[454670d]165        self._index = None
166        if data is not None:
167            self.data = data
168
[60af928]169    @property
170    def data(self):
[4d457df]171        return self.logic.data
[60af928]172
173    @data.setter
174    def data(self, value):
175        """ data setter """
[454670d]176        assert isinstance(value, QtGui.QStandardItem)
[cbcdd2c]177        # _index contains the QIndex with data
[454670d]178        self._index = value
[b1e36a3]179
[4d457df]180        # Update logics with data items
[454670d]181        self.logic.data = GuiUtils.dataFromItem(value)
[4d457df]182
[e1e3e09]183        # Overwrite data type descriptor
184        self.is2D = True if isinstance(self.logic.data, Data2D) else False
185
[5236449]186        self.data_is_loaded = True
[e1e3e09]187
188        # Enable/disable UI components
189        self.setEnablementOnDataLoad()
190
191    def setEnablementOnDataLoad(self):
192        """
193        Enable/disable various UI elements based on data loaded
194        """
[cbcdd2c]195        # Tag along functionality
[b1e36a3]196        self.label.setText("Data loaded from: ")
[a0f5c36]197        self.lblFilename.setText(self.logic.data.filename)
[5236449]198        self.updateQRange()
199        self.cmdFit.setEnabled(True)
[e1e3e09]200        # Switch off Data2D control
201        self.chk2DView.setEnabled(False)
202        self.chk2DView.setVisible(False)
203        self.chkMagnetism.setEnabled(True)
[180bd54]204        # Similarly on other tabs
205        self.options_widget.setEnablementOnDataLoad()
[e1e3e09]206
207        # Smearing tab
208        self.smearing_widget.updateSmearing(self.data)
[60af928]209
[f46f6dc]210    def acceptsData(self):
211        """ Tells the caller this widget can accept new dataset """
[5236449]212        return not self.data_is_loaded
[f46f6dc]213
[6f7f652]214    def disableModelCombo(self):
[cbcdd2c]215        """ Disable the combobox """
[6f7f652]216        self.cbModel.setEnabled(False)
[b1e36a3]217        self.lblModel.setEnabled(False)
[6f7f652]218
219    def enableModelCombo(self):
[cbcdd2c]220        """ Enable the combobox """
[6f7f652]221        self.cbModel.setEnabled(True)
[b1e36a3]222        self.lblModel.setEnabled(True)
[6f7f652]223
224    def disableStructureCombo(self):
[cbcdd2c]225        """ Disable the combobox """
[6f7f652]226        self.cbStructureFactor.setEnabled(False)
[b1e36a3]227        self.lblStructure.setEnabled(False)
[6f7f652]228
229    def enableStructureCombo(self):
[cbcdd2c]230        """ Enable the combobox """
[6f7f652]231        self.cbStructureFactor.setEnabled(True)
[b1e36a3]232        self.lblStructure.setEnabled(True)
[6f7f652]233
[0268aed]234    def togglePoly(self, isChecked):
[454670d]235        """ Enable/disable the polydispersity tab """
[0268aed]236        self.tabFitting.setTabEnabled(TAB_POLY, isChecked)
237
238    def toggleMagnetism(self, isChecked):
[454670d]239        """ Enable/disable the magnetism tab """
[0268aed]240        self.tabFitting.setTabEnabled(TAB_MAGNETISM, isChecked)
241
242    def toggle2D(self, isChecked):
[454670d]243        """ Enable/disable the controls dependent on 1D/2D data instance """
[0268aed]244        self.chkMagnetism.setEnabled(isChecked)
245        self.is2D = isChecked
[1970780]246        # Reload the current model
[e1e3e09]247        if self.kernel_module:
248            self.onSelectModel()
[5236449]249
[86f88d1]250    def initializeControls(self):
251        """
252        Set initial control enablement
253        """
254        self.cmdFit.setEnabled(False)
[d48cc19]255        self.cmdPlot.setEnabled(False)
[180bd54]256        self.options_widget.cmdComputePoints.setVisible(False) # probably redundant
[86f88d1]257        self.chkPolydispersity.setEnabled(True)
258        self.chkPolydispersity.setCheckState(False)
259        self.chk2DView.setEnabled(True)
260        self.chk2DView.setCheckState(False)
261        self.chkMagnetism.setEnabled(False)
262        self.chkMagnetism.setCheckState(False)
[cbcdd2c]263        # Tabs
[86f88d1]264        self.tabFitting.setTabEnabled(TAB_POLY, False)
265        self.tabFitting.setTabEnabled(TAB_MAGNETISM, False)
266        self.lblChi2Value.setText("---")
[e1e3e09]267        # Smearing tab
268        self.smearing_widget.updateSmearing(self.data)
[180bd54]269        # Line edits in the option tab
270        self.updateQRange()
[86f88d1]271
272    def initializeSignals(self):
273        """
274        Connect GUI element signals
275        """
[cbcdd2c]276        # Comboboxes
[cd31251]277        self.cbStructureFactor.currentIndexChanged.connect(self.onSelectStructureFactor)
278        self.cbCategory.currentIndexChanged.connect(self.onSelectCategory)
279        self.cbModel.currentIndexChanged.connect(self.onSelectModel)
[cbcdd2c]280        # Checkboxes
[86f88d1]281        self.chk2DView.toggled.connect(self.toggle2D)
282        self.chkPolydispersity.toggled.connect(self.togglePoly)
283        self.chkMagnetism.toggled.connect(self.toggleMagnetism)
[cbcdd2c]284        # Buttons
[5236449]285        self.cmdFit.clicked.connect(self.onFit)
[cbcdd2c]286        self.cmdPlot.clicked.connect(self.onPlot)
287
288        # Respond to change in parameters from the UI
289        self._model_model.itemChanged.connect(self.updateParamsFromModel)
[cd31251]290        self._poly_model.itemChanged.connect(self.onPolyModelChange)
291        # TODO after the poly_model prototype accepted
292        #self._magnet_model.itemChanged.connect(self.onMagneticModelChange)
[86f88d1]293
[180bd54]294        # Signals from separate tabs asking for replot
295        self.options_widget.plot_signal.connect(self.onOptionsUpdate)
296
[0268aed]297    def onSelectModel(self):
[cbcdd2c]298        """
[0268aed]299        Respond to select Model from list event
[cbcdd2c]300        """
[0268aed]301        model = str(self.cbModel.currentText())
302
303        # Reset structure factor
304        self.cbStructureFactor.setCurrentIndex(0)
305
[f182f93]306        # Reset parameters to fit
307        self.parameters_to_fit = None
[d7ff531]308        self.has_error_column = False
[f182f93]309
[d48cc19]310        # Set enablement on calculate/plot
311        self.cmdPlot.setEnabled(True)
312
[0268aed]313        # SasModel -> QModel
314        self.SASModelToQModel(model)
315
316        if self.data_is_loaded:
[d48cc19]317            self.cmdPlot.setText("Show Plot")
[0268aed]318            self.calculateQGridForModel()
319        else:
[d48cc19]320            self.cmdPlot.setText("Calculate")
[0268aed]321            # Create default datasets if no data passed
322            self.createDefaultDataset()
323
324    def onSelectStructureFactor(self):
325        """
326        Select Structure Factor from list
327        """
328        model = str(self.cbModel.currentText())
329        category = str(self.cbCategory.currentText())
330        structure = str(self.cbStructureFactor.currentText())
331        if category == CATEGORY_STRUCTURE:
332            model = None
333        self.SASModelToQModel(model, structure_factor=structure)
[6f7f652]334
[cd31251]335    def onSelectCategory(self):
[60af928]336        """
337        Select Category from list
338        """
[4d457df]339        category = str(self.cbCategory.currentText())
[86f88d1]340        # Check if the user chose "Choose category entry"
[4d457df]341        if category == CATEGORY_DEFAULT:
[86f88d1]342            # if the previous category was not the default, keep it.
343            # Otherwise, just return
344            if self._previous_category_index != 0:
[351b53e]345                # We need to block signals, or else state changes on perceived unchanged conditions
346                self.cbCategory.blockSignals(True)
[86f88d1]347                self.cbCategory.setCurrentIndex(self._previous_category_index)
[351b53e]348                self.cbCategory.blockSignals(False)
[86f88d1]349            return
350
[4d457df]351        if category == CATEGORY_STRUCTURE:
[6f7f652]352            self.disableModelCombo()
353            self.enableStructureCombo()
[29eb947]354            self._model_model.clear()
[6f7f652]355            return
356
[cbcdd2c]357        # Safely clear and enable the model combo
[6f7f652]358        self.cbModel.blockSignals(True)
359        self.cbModel.clear()
360        self.cbModel.blockSignals(False)
361        self.enableModelCombo()
362        self.disableStructureCombo()
363
[86f88d1]364        self._previous_category_index = self.cbCategory.currentIndex()
[cbcdd2c]365        # Retrieve the list of models
[4d457df]366        model_list = self.master_category_dict[category]
[6f7f652]367        models = []
[cbcdd2c]368        # Populate the models combobox
[b1e36a3]369        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
[4d457df]370
[0268aed]371    def onPolyModelChange(self, item):
372        """
373        Callback method for updating the main model and sasmodel
374        parameters with the GUI values in the polydispersity view
375        """
376        model_column = item.column()
377        model_row = item.row()
378        name_index = self._poly_model.index(model_row, 0)
379        # Extract changed value. Assumes proper validation by QValidator/Delegate
380        # Checkbox in column 0
381        if model_column == 0:
382            value = item.checkState()
383        else:
384            try:
385                value = float(item.text())
386            except ValueError:
387                # Can't be converted properly, bring back the old value and exit
388                return
389
390        parameter_name = str(self._poly_model.data(name_index).toPyObject()) # "distribution of sld" etc.
391        if "Distribution of" in parameter_name:
392            parameter_name = parameter_name[16:]
393        property_name = str(self._poly_model.headerData(model_column, 1).toPyObject()) # Value, min, max, etc.
394        # print "%s(%s) => %d" % (parameter_name, property_name, value)
395
396        # Update the sasmodel
397        #self.kernel_module.params[parameter_name] = value
398
399        # Reload the main model - may not be required if no variable is shown in main view
400        #model = str(self.cbModel.currentText())
401        #self.SASModelToQModel(model)
402
403        pass # debug anchor
404
405    def onFit(self):
406        """
407        Perform fitting on the current data
408        """
[f182f93]409        fitter = Fit()
410
411        # Data going in
412        data = self.logic.data
413        model = self.kernel_module
414        qmin = self.q_range_min
415        qmax = self.q_range_max
416        params_to_fit = self.parameters_to_fit
417
[180bd54]418        # Potential weights added directly to data
[9d266d2]419        self.addWeightingToData(data)
420
[98b13f72]421        # Potential smearing added
[180bd54]422        # Remember that smearing_min/max can be None ->
423        # deal with it until Python gets discriminated unions
[98b13f72]424        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
425
[f182f93]426        # These should be updating somehow?
427        fit_id = 0
428        constraints = []
429        smearer = None
430        page_id = [210]
431        handler = None
432        batch_inputs = {}
433        batch_outputs = {}
434        list_page_id = [page_id]
435        #---------------------------------
436
437        # Parameterize the fitter
438        fitter.set_model(model, fit_id, params_to_fit, data=data,
439                         constraints=constraints)
440        fitter.set_data(data=data, id=fit_id, smearer=smearer, qmin=qmin,
441                        qmax=qmax)
442        fitter.select_problem_for_fit(id=fit_id, value=1)
443
444        fitter.fitter_id = page_id
445
446        # Create the fitting thread, based on the fitter
447        calc_fit = FitThread(handler=handler,
448                             fn=[fitter],
449                             batch_inputs=batch_inputs,
450                             batch_outputs=batch_outputs,
451                             page_id=list_page_id,
452                             updatefn=self.updateFit,
453                             completefn=None)
454
455        # start the trhrhread
456        calc_thread = threads.deferToThread(calc_fit.compute)
457        calc_thread.addCallback(self.fitComplete)
[02ddfb4]458        calc_thread.addErrback(self.fitFailed)
[f182f93]459
460        #disable the Fit button
[d7ff531]461        self.cmdFit.setText('Calculating...')
462        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
[f182f93]463        self.cmdFit.setEnabled(False)
[0268aed]464
[f182f93]465    def updateFit(self):
466        """
467        """
468        print "UPDATE FIT"
[0268aed]469        pass
470
[02ddfb4]471    def fitFailed(self, reason):
472        """
473        """
474        print "FIT FAILED: ", reason
475        pass
476
[f182f93]477    def fitComplete(self, result):
478        """
479        Receive and display fitting results
480        "result" is a tuple of actual result list and the fit time in seconds
481        """
482        #re-enable the Fit button
483        self.cmdFit.setText("Fit")
484        self.cmdFit.setEnabled(True)
[d7ff531]485
486        assert result is not None
487
[f182f93]488        res_list = result[0]
489        res = res_list[0]
490        if res.fitness is None or \
[180bd54]491            not np.isfinite(res.fitness) or \
492            np.any(res.pvec == None) or \
493            not np.all(np.isfinite(res.pvec)):
[f182f93]494            msg = "Fitting did not converge!!!"
[454670d]495            self.communicate.statusBarUpdateSignal.emit(msg)
[f182f93]496            logging.error(msg)
497            return
498
499        elapsed = result[1]
500        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
501        self.communicate.statusBarUpdateSignal.emit(msg)
502
503        fitness = res.fitness
504        param_list = res.param_list
505        param_values = res.pvec
506        param_stderr = res.stderr
507        params_and_errors = zip(param_values, param_stderr)
508        param_dict = dict(izip(param_list, params_and_errors))
509
510        # Dictionary of fitted parameter: value, error
511        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
512        self.updateModelFromList(param_dict)
513
[d7ff531]514        # update charts
515        self.onPlot()
516
[f182f93]517        # Read only value - we can get away by just printing it here
518        chi2_repr = GuiUtils.formatNumber(fitness, high=True)
519        self.lblChi2Value.setText(chi2_repr)
520
521    def iterateOverModel(self, func):
522        """
523        Take func and throw it inside the model row loop
524        """
525        #assert isinstance(func, function)
526        for row_i in xrange(self._model_model.rowCount()):
527            func(row_i)
528
529    def updateModelFromList(self, param_dict):
530        """
531        Update the model with new parameters, create the errors column
532        """
533        assert isinstance(param_dict, dict)
534        if not dict:
535            return
536
[d7ff531]537        def updateFittedValues(row_i):
[f182f93]538            # Utility function for main model update
[d7ff531]539            # internal so can use closure for param_dict
[f182f93]540            param_name = str(self._model_model.item(row_i, 0).text())
541            if param_name not in param_dict.keys():
542                return
543            # modify the param value
[454670d]544            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
545            self._model_model.item(row_i, 1).setText(param_repr)
[f182f93]546            if self.has_error_column:
[454670d]547                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
548                self._model_model.item(row_i, 2).setText(error_repr)
[f182f93]549
[d7ff531]550        def createErrorColumn(row_i):
[f182f93]551            # Utility function for error column update
552            item = QtGui.QStandardItem()
553            for param_name in param_dict.keys():
554                if str(self._model_model.item(row_i, 0).text()) != param_name:
555                    continue
556                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
557                item.setText(error_repr)
558            error_column.append(item)
559
[d7ff531]560        self.iterateOverModel(updateFittedValues)
[f182f93]561
562        if self.has_error_column:
563            return
564
565        error_column = []
[d7ff531]566        self.iterateOverModel(createErrorColumn)
[f182f93]567
[d7ff531]568        # switch off reponse to model change
569        self._model_model.blockSignals(True)
[f182f93]570        self._model_model.insertColumn(2, error_column)
[d7ff531]571        self._model_model.blockSignals(False)
[f182f93]572        FittingUtilities.addErrorHeadersToModel(self._model_model)
[d7ff531]573        # Adjust the table cells width.
574        # TODO: find a way to dynamically adjust column width while resized expanding
575        self.lstParams.resizeColumnToContents(0)
576        self.lstParams.resizeColumnToContents(4)
577        self.lstParams.resizeColumnToContents(5)
578        self.lstParams.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
579
580        self.has_error_column = True
[f182f93]581
[0268aed]582    def onPlot(self):
583        """
584        Plot the current set of data
585        """
[d48cc19]586        # Regardless of previous state, this should now be `plot show` functionality only
587        self.cmdPlot.setText("Show Plot")
588        self.recalculatePlotData()
589        self.showPlot()
590
591    def recalculatePlotData(self):
592        """
593        Generate a new dataset for model
594        """
[180bd54]595        if not self.data_is_loaded:
[0268aed]596            self.createDefaultDataset()
597        self.calculateQGridForModel()
598
[d48cc19]599    def showPlot(self):
600        """
601        Show the current plot in MPL
602        """
603        # Show the chart if ready
604        data_to_show = self.data if self.data_is_loaded else self.model_data
605        if data_to_show is not None:
606            self.communicate.plotRequestedSignal.emit([data_to_show])
607
[180bd54]608    def onOptionsUpdate(self):
[0268aed]609        """
[180bd54]610        Update local option values and replot
[0268aed]611        """
[180bd54]612        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
613            self.options_widget.state()
[61a92d4]614        # set Q range labels on the main tab
615        self.lblMinRangeDef.setText(str(self.q_range_min))
616        self.lblMaxRangeDef.setText(str(self.q_range_max))
[d48cc19]617        self.recalculatePlotData()
[6c8fb2c]618
[0268aed]619    def setDefaultStructureCombo(self):
620        """
621        Fill in the structure factors combo box with defaults
622        """
623        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
624        factors = [factor[0] for factor in structure_factor_list]
625        factors.insert(0, STRUCTURE_DEFAULT)
626        self.cbStructureFactor.clear()
627        self.cbStructureFactor.addItems(sorted(factors))
628
[4d457df]629    def createDefaultDataset(self):
630        """
631        Generate default Dataset 1D/2D for the given model
632        """
633        # Create default datasets if no data passed
634        if self.is2D:
[180bd54]635            qmax = self.q_range_max/np.sqrt(2)
[4d457df]636            qstep = self.npts
637            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
[180bd54]638            return
639        elif self.log_points:
640            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
641            qmax =  10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
642            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
[4d457df]643        else:
[180bd54]644            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
645                    num=self.npts, endpoint=True)
646        self.logic.createDefault1dData(interval, self.tab_id)
[60af928]647
[5236449]648    def readCategoryInfo(self):
[60af928]649        """
650        Reads the categories in from file
651        """
652        self.master_category_dict = defaultdict(list)
653        self.by_model_dict = defaultdict(list)
654        self.model_enabled_dict = defaultdict(bool)
655
[cbcdd2c]656        categorization_file = CategoryInstaller.get_user_file()
657        if not os.path.isfile(categorization_file):
658            categorization_file = CategoryInstaller.get_default_file()
659        with open(categorization_file, 'rb') as cat_file:
[60af928]660            self.master_category_dict = json.load(cat_file)
[5236449]661            self.regenerateModelDict()
[60af928]662
[5236449]663        # Load the model dict
664        models = load_standard_models()
665        for model in models:
666            self.models[model.name] = model
667
668    def regenerateModelDict(self):
[60af928]669        """
[cbcdd2c]670        Regenerates self.by_model_dict which has each model name as the
[60af928]671        key and the list of categories belonging to that model
672        along with the enabled mapping
673        """
674        self.by_model_dict = defaultdict(list)
675        for category in self.master_category_dict:
676            for (model, enabled) in self.master_category_dict[category]:
677                self.by_model_dict[model].append(category)
678                self.model_enabled_dict[model] = enabled
679
[86f88d1]680    def addBackgroundToModel(self, model):
681        """
682        Adds background parameter with default values to the model
683        """
[cbcdd2c]684        assert isinstance(model, QtGui.QStandardItemModel)
[86f88d1]685        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
[4d457df]686        FittingUtilities.addCheckedListToModel(model, checked_list)
[86f88d1]687
688    def addScaleToModel(self, model):
689        """
690        Adds scale parameter with default values to the model
691        """
[cbcdd2c]692        assert isinstance(model, QtGui.QStandardItemModel)
[86f88d1]693        checked_list = ['scale', '1.0', '0.0', 'inf', '']
[4d457df]694        FittingUtilities.addCheckedListToModel(model, checked_list)
[86f88d1]695
[9d266d2]696    def addWeightingToData(self, data):
697        """
698        Adds weighting contribution to fitting data
[180bd54]699        #"""
[e1e3e09]700        # Send original data for weighting
[180bd54]701        weight = get_weight(data=data, is2d=self.is2D, flag=self.weighting)
702        update_module = data.err_data if self.is2D else data.dy
703        update_module = weight
[9d266d2]704
[0268aed]705    def updateQRange(self):
706        """
707        Updates Q Range display
708        """
709        if self.data_is_loaded:
710            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
711        # set Q range labels on the main tab
712        self.lblMinRangeDef.setText(str(self.q_range_min))
713        self.lblMaxRangeDef.setText(str(self.q_range_max))
714        # set Q range labels on the options tab
[180bd54]715        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
[0268aed]716
[4d457df]717    def SASModelToQModel(self, model_name, structure_factor=None):
[60af928]718        """
[cbcdd2c]719        Setting model parameters into table based on selected category
[60af928]720        """
[351b53e]721        # TODO - modify for structure factor-only choice
722
[60af928]723        # Crete/overwrite model items
724        self._model_model.clear()
[5236449]725
[60af928]726        kernel_module = generate.load_kernel_module(model_name)
727        self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
728
[cbcdd2c]729        # Instantiate the current sasmodel
[5236449]730        self.kernel_module = self.models[model_name]()
731
732        # Explicitly add scale and background with default values
[86f88d1]733        self.addScaleToModel(self._model_model)
734        self.addBackgroundToModel(self._model_model)
[60af928]735
[5236449]736        # Update the QModel
[1970780]737        new_rows = FittingUtilities.addParametersToModel(self.model_parameters, self.is2D)
738        for row in new_rows:
739            self._model_model.appendRow(row)
[4d457df]740        # Update the counter used for multishell display
741        self._last_model_row = self._model_model.rowCount()
742
743        FittingUtilities.addHeadersToModel(self._model_model)
[cd31251]744
745        # Add structure factor
746        if structure_factor is not None and structure_factor != "None":
747            structure_module = generate.load_kernel_module(structure_factor)
748            structure_parameters = modelinfo.make_parameter_table(getattr(structure_module, 'parameters', []))
[1970780]749            new_rows = FittingUtilities.addSimpleParametersToModel(structure_parameters, self.is2D)
750            for row in new_rows:
751                self._model_model.appendRow(row)
[4d457df]752            # Update the counter used for multishell display
753            self._last_model_row = self._model_model.rowCount()
[cd31251]754        else:
755            self.addStructureFactor()
756
[5236449]757        # Multishell models need additional treatment
[86f88d1]758        self.addExtraShells()
759
[5236449]760        # Add polydispersity to the model
[86f88d1]761        self.setPolyModel()
[5236449]762        # Add magnetic parameters to the model
[86f88d1]763        self.setMagneticModel()
[5236449]764
[a9b568c]765        # Adjust the table cells width
766        self.lstParams.resizeColumnToContents(0)
767        self.lstParams.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
768
[5236449]769        # Now we claim the model has been loaded
[86f88d1]770        self.model_is_loaded = True
771
[5236449]772        # Update Q Ranges
773        self.updateQRange()
774
[cd31251]775    def updateParamsFromModel(self, item):
776        """
777        Callback method for updating the sasmodel parameters with the GUI values
778        """
[cbcdd2c]779        model_column = item.column()
[cd31251]780
781        if model_column == 0:
[f182f93]782            self.checkboxSelected(item)
[cd31251]783            return
784
[f182f93]785        model_row = item.row()
786        name_index = self._model_model.index(model_row, 0)
787
[cd31251]788        # Extract changed value. Assumes proper validation by QValidator/Delegate
789        value = float(item.text())
[cbcdd2c]790        parameter_name = str(self._model_model.data(name_index).toPyObject()) # sld, background etc.
791        property_name = str(self._model_model.headerData(1, model_column).toPyObject()) # Value, min, max, etc.
792
793        self.kernel_module.params[parameter_name] = value
794
795        # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
796        # magnetic params in self.kernel_module.details['M0:parameter_name'] = value
797        # multishell params in self.kernel_module.details[??] = value
798
[d7ff531]799        # Force the chart update when actual parameters changed
800        if model_column == 1:
[d48cc19]801            self.recalculatePlotData()
[7d077d1]802
[f182f93]803    def checkboxSelected(self, item):
804        # Assure we're dealing with checkboxes
805        if not item.isCheckable():
806            return
807        status = item.checkState()
808
809        def isChecked(row):
810            return self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked
811
812        def isCheckable(row):
813            return self._model_model.item(row, 0).isCheckable()
814
815        # If multiple rows selected - toggle all of them, filtering uncheckable
816        rows = [s.row() for s in self.lstParams.selectionModel().selectedRows() if isCheckable(s.row())]
817
818        # Switch off signaling from the model to avoid recursion
819        self._model_model.blockSignals(True)
820        # Convert to proper indices and set requested enablement
821        items = [self._model_model.item(row, 0).setCheckState(status) for row in rows]
822        self._model_model.blockSignals(False)
823
824        # update the list of parameters to fit
825        self.parameters_to_fit = [str(self._model_model.item(row_index, 0).text())
826                                  for row_index in xrange(self._model_model.rowCount())
827                                  if isChecked(row_index)]
828
[6fd4e36]829    def nameForFittedData(self, name):
[5236449]830        """
[6fd4e36]831        Generate name for the current fit
[5236449]832        """
833        if self.is2D:
834            name += "2d"
835        name = "M%i [%s]" % (self.tab_id, name)
[6fd4e36]836        return name
837
838    def createNewIndex(self, fitted_data):
839        """
840        Create a model or theory index with passed Data1D/Data2D
841        """
842        if self.data_is_loaded:
[0268aed]843            if not fitted_data.name:
844                name = self.nameForFittedData(self.data.filename)
845                fitted_data.title = name
846                fitted_data.name = name
847                fitted_data.filename = name
[7d077d1]848                fitted_data.symbol = "Line"
[6fd4e36]849            self.updateModelIndex(fitted_data)
850        else:
[0268aed]851            name = self.nameForFittedData(self.kernel_module.name)
852            fitted_data.title = name
853            fitted_data.name = name
854            fitted_data.filename = name
855            fitted_data.symbol = "Line"
[6fd4e36]856            self.createTheoryIndex(fitted_data)
857
858    def updateModelIndex(self, fitted_data):
859        """
860        Update a QStandardModelIndex containing model data
861        """
[0268aed]862        if fitted_data.name is None:
863            name = self.nameForFittedData(self.logic.data.filename)
864            fitted_data.title = name
865            fitted_data.name = name
866        else:
867            name = fitted_data.name
868        # Make this a line if no other defined
[7d077d1]869        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
[0268aed]870            fitted_data.symbol = 'Line'
[6fd4e36]871        # Notify the GUI manager so it can update the main model in DataExplorer
872        GuiUtils.updateModelItemWithPlot(self._index, QtCore.QVariant(fitted_data), name)
873
874    def createTheoryIndex(self, fitted_data):
875        """
876        Create a QStandardModelIndex containing model data
877        """
[0268aed]878        if fitted_data.name is None:
879            name = self.nameForFittedData(self.kernel_module.name)
880            fitted_data.title = name
881            fitted_data.name = name
882            fitted_data.filename = name
883        else:
884            name = fitted_data.name
[6fd4e36]885        # Notify the GUI manager so it can create the theory model in DataExplorer
886        new_item = GuiUtils.createModelItemWithPlot(QtCore.QVariant(fitted_data), name=name)
[cbcdd2c]887        self.communicate.updateTheoryFromPerspectiveSignal.emit(new_item)
[5236449]888
[4d457df]889    def methodCalculateForData(self):
890        '''return the method for data calculation'''
891        return Calc1D if isinstance(self.data, Data1D) else Calc2D
892
893    def methodCompleteForData(self):
894        '''return the method for result parsin on calc complete '''
895        return self.complete1D if isinstance(self.data, Data1D) else self.complete2D
896
[b1e36a3]897    def calculateQGridForModel(self):
[86f88d1]898        """
899        Prepare the fitting data object, based on current ModelModel
900        """
[180bd54]901        if self.kernel_module is None:
902            return
[4d457df]903        # Awful API to a backend method.
904        method = self.methodCalculateForData()(data=self.data,
[cbcdd2c]905                              model=self.kernel_module,
906                              page_id=0,
907                              qmin=self.q_range_min,
908                              qmax=self.q_range_max,
909                              smearer=None,
910                              state=None,
911                              weight=None,
912                              fid=None,
913                              toggle_mode_on=False,
[4d457df]914                              completefn=None,
[cbcdd2c]915                              update_chisqr=True,
[5236449]916                              exception_handler=self.calcException,
[cbcdd2c]917                              source=None)
[4d457df]918
919        calc_thread = threads.deferToThread(method.compute)
920        calc_thread.addCallback(self.methodCompleteForData())
[5236449]921
[cbcdd2c]922    def complete1D(self, return_data):
[5236449]923        """
[4d457df]924        Plot the current 1D data
925        """
[d48cc19]926        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
927        self.calculateResiduals(fitted_data)
928        self.model_data = fitted_data
[cbcdd2c]929
930    def complete2D(self, return_data):
931        """
[4d457df]932        Plot the current 2D data
933        """
[6fd4e36]934        fitted_data = self.logic.new2DPlot(return_data)
935        self.calculateResiduals(fitted_data)
[d48cc19]936        self.model_data = fitted_data
[6fd4e36]937
938    def calculateResiduals(self, fitted_data):
939        """
940        Calculate and print Chi2 and display chart of residuals
941        """
942        # Create a new index for holding data
[7d077d1]943        fitted_data.symbol = "Line"
[6fd4e36]944        self.createNewIndex(fitted_data)
945        # Calculate difference between return_data and logic.data
946        chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
947        # Update the control
[f182f93]948        chi2_repr = "---" if chi2 is None else GuiUtils.formatNumber(chi2, high=True)
949        self.lblChi2Value.setText(chi2_repr)
[cbcdd2c]950
[d48cc19]951        self.communicate.plotUpdateSignal.emit([fitted_data])
952
[0268aed]953        # Plot residuals if actual data
954        if self.data_is_loaded:
955            residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
[7d077d1]956            residuals_plot.id = "Residual " + residuals_plot.id
[0268aed]957            self.createNewIndex(residuals_plot)
[9f25bce]958            self.communicate.plotUpdateSignal.emit([residuals_plot])
[5236449]959
960    def calcException(self, etype, value, tb):
961        """
[b1e36a3]962        Something horrible happened in the deferred.
[5236449]963        """
964        logging.error("".join(traceback.format_exception(etype, value, tb)))
[60af928]965
966    def setTableProperties(self, table):
967        """
968        Setting table properties
969        """
970        # Table properties
971        table.verticalHeader().setVisible(False)
972        table.setAlternatingRowColors(True)
973        table.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
974        table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
[f46f6dc]975        table.resizeColumnsToContents()
976
[60af928]977        # Header
978        header = table.horizontalHeader()
[f46f6dc]979        header.setResizeMode(QtGui.QHeaderView.ResizeToContents)
980
981        header.ResizeMode(QtGui.QHeaderView.Interactive)
[b1e36a3]982        # Resize column 0 and 6 to content
[f46f6dc]983        header.setResizeMode(0, QtGui.QHeaderView.ResizeToContents)
984        header.setResizeMode(6, QtGui.QHeaderView.ResizeToContents)
[60af928]985
986    def setPolyModel(self):
987        """
988        Set polydispersity values
989        """
[86f88d1]990        if not self.model_parameters:
991            return
992        self._poly_model.clear()
993        for row, param in enumerate(self.model_parameters.form_volume_parameters):
994            # Counters should not be included
995            if not param.polydisperse:
996                continue
997
998            # Potential multishell params
999            checked_list = ["Distribution of "+param.name, str(param.default),
[cbcdd2c]1000                            str(param.limits[0]), str(param.limits[1]),
[86f88d1]1001                            "35", "3", ""]
[4d457df]1002            FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
[86f88d1]1003
1004            #TODO: Need to find cleaner way to input functions
1005            func = QtGui.QComboBox()
[cbcdd2c]1006            func.addItems(['rectangle', 'array', 'lognormal', 'gaussian', 'schulz',])
1007            func_index = self.lstPoly.model().index(row, 6)
1008            self.lstPoly.setIndexWidget(func_index, func)
[86f88d1]1009
[4d457df]1010        FittingUtilities.addPolyHeadersToModel(self._poly_model)
[60af928]1011
1012    def setMagneticModel(self):
1013        """
1014        Set magnetism values on model
1015        """
[86f88d1]1016        if not self.model_parameters:
1017            return
1018        self._magnet_model.clear()
1019        for param in self.model_parameters.call_parameters:
1020            if param.type != "magnetic":
1021                continue
1022            checked_list = [param.name,
1023                            str(param.default),
1024                            str(param.limits[0]),
1025                            str(param.limits[1]),
1026                            param.units]
[4d457df]1027            FittingUtilities.addCheckedListToModel(self._magnet_model, checked_list)
[86f88d1]1028
[4d457df]1029        FittingUtilities.addHeadersToModel(self._magnet_model)
[60af928]1030
[cd31251]1031    def addStructureFactor(self):
1032        """
1033        Add structure factors to the list of parameters
1034        """
1035        if self.kernel_module.is_form_factor:
1036            self.enableStructureCombo()
1037        else:
1038            self.disableStructureCombo()
1039
[60af928]1040    def addExtraShells(self):
1041        """
[f46f6dc]1042        Add a combobox for multiple shell display
[60af928]1043        """
[4d457df]1044        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
[f46f6dc]1045
1046        if param_length == 0:
1047            return
1048
[6f7f652]1049        # cell 1: variable name
[f46f6dc]1050        item1 = QtGui.QStandardItem(param_name)
1051
[60af928]1052        func = QtGui.QComboBox()
[b1e36a3]1053        # Available range of shells displayed in the combobox
1054        func.addItems([str(i) for i in xrange(param_length+1)])
[a9b568c]1055
[b1e36a3]1056        # Respond to index change
[86f88d1]1057        func.currentIndexChanged.connect(self.modifyShellsInList)
[60af928]1058
[6f7f652]1059        # cell 2: combobox
[f46f6dc]1060        item2 = QtGui.QStandardItem()
1061        self._model_model.appendRow([item1, item2])
[60af928]1062
[6f7f652]1063        # Beautify the row:  span columns 2-4
[60af928]1064        shell_row = self._model_model.rowCount()
[f46f6dc]1065        shell_index = self._model_model.index(shell_row-1, 1)
[86f88d1]1066
[4d457df]1067        self.lstParams.setIndexWidget(shell_index, func)
[86f88d1]1068        self._last_model_row = self._model_model.rowCount()
1069
[a9b568c]1070        # Set the index to the state-kept value
1071        func.setCurrentIndex(self.current_shell_displayed
1072                             if self.current_shell_displayed < func.count() else 0)
1073
[86f88d1]1074    def modifyShellsInList(self, index):
1075        """
1076        Add/remove additional multishell parameters
1077        """
1078        # Find row location of the combobox
1079        last_row = self._last_model_row
1080        remove_rows = self._model_model.rowCount() - last_row
1081
1082        if remove_rows > 1:
1083            self._model_model.removeRows(last_row, remove_rows)
1084
[4d457df]1085        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
[a9b568c]1086        self.current_shell_displayed = index
[60af928]1087
Note: See TracBrowser for help on using the repository browser.