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

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

Minor improvements to the fitting widget

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