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

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

Putting files in more ordered fashion

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