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

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

Refactor instrumental smearing tab + make it functional

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