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

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

Add display of orientational parameters + minor refactoring

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