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

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

Added more unit tests for SASVIEW-569

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