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

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

Custom delegate for fancy display of units

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