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

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 0215e0a was 180bd54, checked in by Piotr Rozyczko <rozyczko@…>, 8 years ago

Refactored fitting options tab

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