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

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

Added data weighting prototype

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