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

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

Code review fixes SASVIEW-588
Pylint related fixes in Perspectives/Fitting?

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