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

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

Improvements to view delegates and model updates.

  • Property mode set to 100644
File size: 47.0 KB
Line 
1import json
2import os
3from collections import defaultdict
4from itertools import izip
5
6import logging
7import traceback
8from twisted.internet import threads
9import numpy as np
10
11from PyQt4 import QtGui
12from PyQt4 import QtCore
13from PyQt4 import QtWebKit
14
15from sasmodels import generate
16from sasmodels import modelinfo
17from sasmodels.sasview_model import load_standard_models
18from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
19from sas.sasgui.perspectives.fitting.fit_thread import FitThread
20
21from sas.sasgui.guiframe.CategoryInstaller import CategoryInstaller
22from sas.sasgui.guiframe.dataFitting import Data1D
23from sas.sasgui.guiframe.dataFitting import Data2D
24import sas.qtgui.Utilities.GuiUtils as GuiUtils
25from sas.sasgui.perspectives.fitting.model_thread import Calc1D
26from sas.sasgui.perspectives.fitting.model_thread import Calc2D
27from sas.sasgui.perspectives.fitting.utils import get_weight
28
29from sas.qtgui.Perspectives.Fitting.UI.FittingWidgetUI import Ui_FittingWidgetUI
30from sas.qtgui.Perspectives.Fitting.FittingLogic import FittingLogic
31from sas.qtgui.Perspectives.Fitting import FittingUtilities
32from sas.qtgui.Perspectives.Fitting.SmearingWidget import SmearingWidget
33from sas.qtgui.Perspectives.Fitting.OptionsWidget import OptionsWidget
34from sas.qtgui.Perspectives.Fitting.FitPage import FitPage
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        self.updateUndo()
373
374    def onSelectStructureFactor(self):
375        """
376        Select Structure Factor from list
377        """
378        model = str(self.cbModel.currentText())
379        category = str(self.cbCategory.currentText())
380        structure = str(self.cbStructureFactor.currentText())
381        if category == CATEGORY_STRUCTURE:
382            model = None
383        self.SASModelToQModel(model, structure_factor=structure)
384
385    def onSelectCategory(self):
386        """
387        Select Category from list
388        """
389        category = str(self.cbCategory.currentText())
390        # Check if the user chose "Choose category entry"
391        if category == CATEGORY_DEFAULT:
392            # if the previous category was not the default, keep it.
393            # Otherwise, just return
394            if self._previous_category_index != 0:
395                # We need to block signals, or else state changes on perceived unchanged conditions
396                self.cbCategory.blockSignals(True)
397                self.cbCategory.setCurrentIndex(self._previous_category_index)
398                self.cbCategory.blockSignals(False)
399            return
400
401        if category == CATEGORY_STRUCTURE:
402            self.disableModelCombo()
403            self.enableStructureCombo()
404            self._model_model.clear()
405            return
406
407        # Safely clear and enable the model combo
408        self.cbModel.blockSignals(True)
409        self.cbModel.clear()
410        self.cbModel.blockSignals(False)
411        self.enableModelCombo()
412        self.disableStructureCombo()
413
414        self._previous_category_index = self.cbCategory.currentIndex()
415        # Retrieve the list of models
416        model_list = self.master_category_dict[category]
417        # Populate the models combobox
418        self.cbModel.addItems(sorted([model for (model, _) in model_list]))
419
420    def onPolyModelChange(self, item):
421        """
422        Callback method for updating the main model and sasmodel
423        parameters with the GUI values in the polydispersity view
424        """
425        model_column = item.column()
426        model_row = item.row()
427        name_index = self._poly_model.index(model_row, 0)
428        # Extract changed value. Assumes proper validation by QValidator/Delegate
429        # TODO: abstract away hardcoded column numbers
430        if model_column == 0:
431            # Is the parameter checked for fitting?
432            value = item.checkState()
433            # TODO: add the param to self.params_for_fitting
434        elif model_column == 6:
435            value = item.text()
436            # TODO: Modify Npts/Nsigs based on function choice
437        else:
438            try:
439                value = float(item.text())
440            except ValueError:
441                # Can't be converted properly, bring back the old value and exit
442                return
443
444        parameter_name = str(self._poly_model.data(name_index).toPyObject()) # "distribution of sld" etc.
445        if "Distribution of" in parameter_name:
446            parameter_name = parameter_name[16:]
447        property_name = str(self._poly_model.headerData(model_column, 1).toPyObject()) # Value, min, max, etc.
448        # print "%s(%s) => %d" % (parameter_name, property_name, value)
449
450        # Update the sasmodel
451        #self.kernel_module.params[parameter_name] = value
452
453        # Reload the main model - may not be required if no variable is shown in main view
454        #model = str(self.cbModel.currentText())
455        #self.SASModelToQModel(model)
456
457        pass # debug anchor
458
459    def onHelp(self):
460        """
461        Show the "Fitting" section of help
462        """
463        tree_location = self.parent.HELP_DIRECTORY_LOCATION +\
464            "/user/sasgui/perspectives/fitting/fitting_help.html"
465        self.helpView.load(QtCore.QUrl(tree_location))
466        self.helpView.show()
467
468    def onFit(self):
469        """
470        Perform fitting on the current data
471        """
472        fitter = Fit()
473
474        # Data going in
475        data = self.logic.data
476        model = self.kernel_module
477        qmin = self.q_range_min
478        qmax = self.q_range_max
479        params_to_fit = self.parameters_to_fit
480
481        # Potential weights added directly to data
482        self.addWeightingToData(data)
483
484        # Potential smearing added
485        # Remember that smearing_min/max can be None ->
486        # deal with it until Python gets discriminated unions
487        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
488
489        # These should be updating somehow?
490        fit_id = 0
491        constraints = []
492        smearer = None
493        page_id = [210]
494        handler = None
495        batch_inputs = {}
496        batch_outputs = {}
497        list_page_id = [page_id]
498        #---------------------------------
499
500        # Parameterize the fitter
501        fitter.set_model(model, fit_id, params_to_fit, data=data,
502                         constraints=constraints)
503
504        fitter.set_data(data=data, id=fit_id, smearer=smearer, qmin=qmin,
505                        qmax=qmax)
506        fitter.select_problem_for_fit(id=fit_id, value=1)
507
508        fitter.fitter_id = page_id
509
510        # Create the fitting thread, based on the fitter
511        calc_fit = FitThread(handler=handler,
512                             fn=[fitter],
513                             batch_inputs=batch_inputs,
514                             batch_outputs=batch_outputs,
515                             page_id=list_page_id,
516                             updatefn=self.updateFit,
517                             completefn=None)
518
519        # start the trhrhread
520        calc_thread = threads.deferToThread(calc_fit.compute)
521        calc_thread.addCallback(self.fitComplete)
522        calc_thread.addErrback(self.fitFailed)
523
524        #disable the Fit button
525        self.cmdFit.setText('Calculating...')
526        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
527        self.cmdFit.setEnabled(False)
528
529    def updateFit(self):
530        """
531        """
532        print "UPDATE FIT"
533        pass
534
535    def fitFailed(self, reason):
536        """
537        """
538        print "FIT FAILED: ", reason
539        pass
540
541    def fitComplete(self, result):
542        """
543        Receive and display fitting results
544        "result" is a tuple of actual result list and the fit time in seconds
545        """
546        #re-enable the Fit button
547        self.cmdFit.setText("Fit")
548        self.cmdFit.setEnabled(True)
549
550        assert result is not None
551
552        res_list = result[0]
553        res = res_list[0]
554        if res.fitness is None or \
555            not np.isfinite(res.fitness) or \
556            np.any(res.pvec is None) or \
557            not np.all(np.isfinite(res.pvec)):
558            msg = "Fitting did not converge!!!"
559            self.communicate.statusBarUpdateSignal.emit(msg)
560            logging.error(msg)
561            return
562
563        elapsed = result[1]
564        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
565        self.communicate.statusBarUpdateSignal.emit(msg)
566
567        self.chi2 = res.fitness
568        param_list = res.param_list
569        param_values = res.pvec
570        param_stderr = res.stderr
571        params_and_errors = zip(param_values, param_stderr)
572        param_dict = dict(izip(param_list, params_and_errors))
573
574        # Dictionary of fitted parameter: value, error
575        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
576        self.updateModelFromList(param_dict)
577
578        # update charts
579        self.onPlot()
580
581        # Read only value - we can get away by just printing it here
582        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
583        self.lblChi2Value.setText(chi2_repr)
584
585    def iterateOverModel(self, func):
586        """
587        Take func and throw it inside the model row loop
588        """
589        #assert isinstance(func, function)
590        for row_i in xrange(self._model_model.rowCount()):
591            func(row_i)
592
593    def updateModelFromList(self, param_dict):
594        """
595        Update the model with new parameters, create the errors column
596        """
597        assert isinstance(param_dict, dict)
598        if not dict:
599            return
600
601        def updateFittedValues(row_i):
602            # Utility function for main model update
603            # internal so can use closure for param_dict
604            param_name = str(self._model_model.item(row_i, 0).text())
605            if param_name not in param_dict.keys():
606                return
607            # modify the param value
608            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
609            self._model_model.item(row_i, 1).setText(param_repr)
610            if self.has_error_column:
611                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
612                self._model_model.item(row_i, 2).setText(error_repr)
613
614        def createErrorColumn(row_i):
615            # Utility function for error column update
616            item = QtGui.QStandardItem()
617            for param_name in param_dict.keys():
618                if str(self._model_model.item(row_i, 0).text()) != param_name:
619                    continue
620                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
621                item.setText(error_repr)
622            error_column.append(item)
623
624        # block signals temporarily, so we don't end up
625        # updating charts with every single model change on the end of fitting
626        self._model_model.blockSignals(True)
627        self.iterateOverModel(updateFittedValues)
628        self._model_model.blockSignals(False)
629
630        if self.has_error_column:
631            return
632
633        error_column = []
634        self.iterateOverModel(createErrorColumn)
635
636        # switch off reponse to model change
637        self._model_model.blockSignals(True)
638        self._model_model.insertColumn(2, error_column)
639        self._model_model.blockSignals(False)
640        FittingUtilities.addErrorHeadersToModel(self._model_model)
641        # Adjust the table cells width.
642        # TODO: find a way to dynamically adjust column width while resized expanding
643        self.lstParams.resizeColumnToContents(0)
644        self.lstParams.resizeColumnToContents(4)
645        self.lstParams.resizeColumnToContents(5)
646        self.lstParams.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
647
648        self.has_error_column = True
649
650    def onPlot(self):
651        """
652        Plot the current set of data
653        """
654        # Regardless of previous state, this should now be `plot show` functionality only
655        self.cmdPlot.setText("Show Plot")
656        if not self.data_is_loaded:
657            self.recalculatePlotData()
658        self.showPlot()
659
660    def recalculatePlotData(self):
661        """
662        Generate a new dataset for model
663        """
664        if not self.data_is_loaded:
665            self.createDefaultDataset()
666        self.calculateQGridForModel()
667
668    def showPlot(self):
669        """
670        Show the current plot in MPL
671        """
672        # Show the chart if ready
673        data_to_show = self.data if self.data_is_loaded else self.model_data
674        if data_to_show is not None:
675            self.communicate.plotRequestedSignal.emit([data_to_show])
676
677    def onOptionsUpdate(self):
678        """
679        Update local option values and replot
680        """
681        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
682            self.options_widget.state()
683        # set Q range labels on the main tab
684        self.lblMinRangeDef.setText(str(self.q_range_min))
685        self.lblMaxRangeDef.setText(str(self.q_range_max))
686        self.recalculatePlotData()
687
688    def setDefaultStructureCombo(self):
689        """
690        Fill in the structure factors combo box with defaults
691        """
692        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
693        factors = [factor[0] for factor in structure_factor_list]
694        factors.insert(0, STRUCTURE_DEFAULT)
695        self.cbStructureFactor.clear()
696        self.cbStructureFactor.addItems(sorted(factors))
697
698    def createDefaultDataset(self):
699        """
700        Generate default Dataset 1D/2D for the given model
701        """
702        # Create default datasets if no data passed
703        if self.is2D:
704            qmax = self.q_range_max/np.sqrt(2)
705            qstep = self.npts
706            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
707            return
708        elif self.log_points:
709            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
710            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
711            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
712        else:
713            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
714                                   num=self.npts, endpoint=True)
715        self.logic.createDefault1dData(interval, self.tab_id)
716
717    def readCategoryInfo(self):
718        """
719        Reads the categories in from file
720        """
721        self.master_category_dict = defaultdict(list)
722        self.by_model_dict = defaultdict(list)
723        self.model_enabled_dict = defaultdict(bool)
724
725        categorization_file = CategoryInstaller.get_user_file()
726        if not os.path.isfile(categorization_file):
727            categorization_file = CategoryInstaller.get_default_file()
728        with open(categorization_file, 'rb') as cat_file:
729            self.master_category_dict = json.load(cat_file)
730            self.regenerateModelDict()
731
732        # Load the model dict
733        models = load_standard_models()
734        for model in models:
735            self.models[model.name] = model
736
737    def regenerateModelDict(self):
738        """
739        Regenerates self.by_model_dict which has each model name as the
740        key and the list of categories belonging to that model
741        along with the enabled mapping
742        """
743        self.by_model_dict = defaultdict(list)
744        for category in self.master_category_dict:
745            for (model, enabled) in self.master_category_dict[category]:
746                self.by_model_dict[model].append(category)
747                self.model_enabled_dict[model] = enabled
748
749    def addBackgroundToModel(self, model):
750        """
751        Adds background parameter with default values to the model
752        """
753        assert isinstance(model, QtGui.QStandardItemModel)
754        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
755        FittingUtilities.addCheckedListToModel(model, checked_list)
756        last_row = model.rowCount()-1
757        model.item(last_row, 0).setEditable(False)
758        model.item(last_row, 4).setEditable(False)
759
760    def addScaleToModel(self, model):
761        """
762        Adds scale parameter with default values to the model
763        """
764        assert isinstance(model, QtGui.QStandardItemModel)
765        checked_list = ['scale', '1.0', '0.0', 'inf', '']
766        FittingUtilities.addCheckedListToModel(model, checked_list)
767        last_row = model.rowCount()-1
768        model.item(last_row, 0).setEditable(False)
769        model.item(last_row, 4).setEditable(False)
770
771    def addWeightingToData(self, data):
772        """
773        Adds weighting contribution to fitting data
774        """
775        # Send original data for weighting
776        weight = get_weight(data=data, is2d=self.is2D, flag=self.weighting)
777        update_module = data.err_data if self.is2D else data.dy
778        update_module = weight
779
780    def updateQRange(self):
781        """
782        Updates Q Range display
783        """
784        if self.data_is_loaded:
785            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
786        # set Q range labels on the main tab
787        self.lblMinRangeDef.setText(str(self.q_range_min))
788        self.lblMaxRangeDef.setText(str(self.q_range_max))
789        # set Q range labels on the options tab
790        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
791
792    def SASModelToQModel(self, model_name, structure_factor=None):
793        """
794        Setting model parameters into table based on selected category
795        """
796        # TODO - modify for structure factor-only choice
797
798        # Crete/overwrite model items
799        self._model_model.clear()
800
801        kernel_module = generate.load_kernel_module(model_name)
802        self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
803
804        # Instantiate the current sasmodel
805        self.kernel_module = self.models[model_name]()
806
807        # Explicitly add scale and background with default values
808        self.addScaleToModel(self._model_model)
809        self.addBackgroundToModel(self._model_model)
810
811        # Update the QModel
812        new_rows = FittingUtilities.addParametersToModel(self.model_parameters, self.is2D)
813        for row in new_rows:
814            self._model_model.appendRow(row)
815        # Update the counter used for multishell display
816        self._last_model_row = self._model_model.rowCount()
817
818        FittingUtilities.addHeadersToModel(self._model_model)
819
820        # Add structure factor
821        if structure_factor is not None and structure_factor != "None":
822            structure_module = generate.load_kernel_module(structure_factor)
823            structure_parameters = modelinfo.make_parameter_table(getattr(structure_module, 'parameters', []))
824            new_rows = FittingUtilities.addSimpleParametersToModel(structure_parameters, self.is2D)
825            for row in new_rows:
826                self._model_model.appendRow(row)
827            # Update the counter used for multishell display
828            self._last_model_row = self._model_model.rowCount()
829        else:
830            self.addStructureFactor()
831
832        # Multishell models need additional treatment
833        self.addExtraShells()
834
835        # Add polydispersity to the model
836        self.setPolyModel()
837        # Add magnetic parameters to the model
838        self.setMagneticModel()
839
840        # Adjust the table cells width
841        self.lstParams.resizeColumnToContents(0)
842        self.lstParams.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
843
844        # Now we claim the model has been loaded
845        self.model_is_loaded = True
846
847        # Update Q Ranges
848        self.updateQRange()
849
850    def updateParamsFromModel(self, item):
851        """
852        Callback method for updating the sasmodel parameters with the GUI values
853        """
854        model_column = item.column()
855
856        if model_column == 0:
857            self.checkboxSelected(item)
858            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
859            return
860
861        model_row = item.row()
862        name_index = self._model_model.index(model_row, 0)
863
864        # Extract changed value. Assumes proper validation by QValidator/Delegate
865        try:
866            value = float(item.text())
867        except ValueError:
868            # Unparsable field
869            return
870        parameter_name = str(self._model_model.data(name_index).toPyObject()) # sld, background etc.
871        property_index = self._model_model.headerData(1, model_column).toInt()[0]-1 # Value, min, max, etc.
872
873        # Update the parameter value - note: this supports +/-inf as well
874        self.kernel_module.params[parameter_name] = value
875
876        # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
877        self.kernel_module.details[parameter_name][property_index] = value
878
879        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
880        # TODO: multishell params in self.kernel_module.details[??] = value
881
882        # Force the chart update when actual parameters changed
883        if model_column == 1:
884            self.recalculatePlotData()
885
886        # Update state stack
887        self.updateUndo()
888
889    def checkboxSelected(self, item):
890        # Assure we're dealing with checkboxes
891        if not item.isCheckable():
892            return
893        status = item.checkState()
894
895        def isChecked(row):
896            return self._model_model.item(row, 0).checkState() == QtCore.Qt.Checked
897
898        def isCheckable(row):
899            return self._model_model.item(row, 0).isCheckable()
900
901        # If multiple rows selected - toggle all of them, filtering uncheckable
902        rows = [s.row() for s in self.lstParams.selectionModel().selectedRows() if isCheckable(s.row())]
903
904        # Switch off signaling from the model to avoid recursion
905        self._model_model.blockSignals(True)
906        # Convert to proper indices and set requested enablement
907        _ = [self._model_model.item(row, 0).setCheckState(status) for row in rows]
908        self._model_model.blockSignals(False)
909
910        # update the list of parameters to fit
911        self.parameters_to_fit = [str(self._model_model.item(row_index, 0).text())
912                                  for row_index in xrange(self._model_model.rowCount())
913                                  if isChecked(row_index)]
914
915    def nameForFittedData(self, name):
916        """
917        Generate name for the current fit
918        """
919        if self.is2D:
920            name += "2d"
921        name = "M%i [%s]" % (self.tab_id, name)
922        return name
923
924    def createNewIndex(self, fitted_data):
925        """
926        Create a model or theory index with passed Data1D/Data2D
927        """
928        if self.data_is_loaded:
929            if not fitted_data.name:
930                name = self.nameForFittedData(self.data.filename)
931                fitted_data.title = name
932                fitted_data.name = name
933                fitted_data.filename = name
934                fitted_data.symbol = "Line"
935            self.updateModelIndex(fitted_data)
936        else:
937            name = self.nameForFittedData(self.kernel_module.name)
938            fitted_data.title = name
939            fitted_data.name = name
940            fitted_data.filename = name
941            fitted_data.symbol = "Line"
942            self.createTheoryIndex(fitted_data)
943
944    def updateModelIndex(self, fitted_data):
945        """
946        Update a QStandardModelIndex containing model data
947        """
948        name = self.nameFromData(fitted_data)
949        # Make this a line if no other defined
950        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
951            fitted_data.symbol = 'Line'
952        # Notify the GUI manager so it can update the main model in DataExplorer
953        GuiUtils.updateModelItemWithPlot(self._index, QtCore.QVariant(fitted_data), name)
954
955    def createTheoryIndex(self, fitted_data):
956        """
957        Create a QStandardModelIndex containing model data
958        """
959        name = self.nameFromData(fitted_data)
960        # Notify the GUI manager so it can create the theory model in DataExplorer
961        new_item = GuiUtils.createModelItemWithPlot(QtCore.QVariant(fitted_data), name=name)
962        self.communicate.updateTheoryFromPerspectiveSignal.emit(new_item)
963
964    def nameFromData(self, fitted_data):
965        """
966        Return name for the dataset. Terribly impure function.
967        """
968        if fitted_data.name is None:
969            name = self.nameForFittedData(self.logic.data.filename)
970            fitted_data.title = name
971            fitted_data.name = name
972            fitted_data.filename = name
973        else:
974            name = fitted_data.name
975        return name
976
977    def methodCalculateForData(self):
978        '''return the method for data calculation'''
979        return Calc1D if isinstance(self.data, Data1D) else Calc2D
980
981    def methodCompleteForData(self):
982        '''return the method for result parsin on calc complete '''
983        return self.complete1D if isinstance(self.data, Data1D) else self.complete2D
984
985    def calculateQGridForModel(self):
986        """
987        Prepare the fitting data object, based on current ModelModel
988        """
989        if self.kernel_module is None:
990            return
991        # Awful API to a backend method.
992        method = self.methodCalculateForData()(data=self.data,
993                                               model=self.kernel_module,
994                                               page_id=0,
995                                               qmin=self.q_range_min,
996                                               qmax=self.q_range_max,
997                                               smearer=None,
998                                               state=None,
999                                               weight=None,
1000                                               fid=None,
1001                                               toggle_mode_on=False,
1002                                               completefn=None,
1003                                               update_chisqr=True,
1004                                               exception_handler=self.calcException,
1005                                               source=None)
1006
1007        calc_thread = threads.deferToThread(method.compute)
1008        calc_thread.addCallback(self.methodCompleteForData())
1009
1010    def complete1D(self, return_data):
1011        """
1012        Plot the current 1D data
1013        """
1014        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
1015        self.calculateResiduals(fitted_data)
1016        self.model_data = fitted_data
1017
1018    def complete2D(self, return_data):
1019        """
1020        Plot the current 2D data
1021        """
1022        fitted_data = self.logic.new2DPlot(return_data)
1023        self.calculateResiduals(fitted_data)
1024        self.model_data = fitted_data
1025
1026    def calculateResiduals(self, fitted_data):
1027        """
1028        Calculate and print Chi2 and display chart of residuals
1029        """
1030        # Create a new index for holding data
1031        fitted_data.symbol = "Line"
1032        self.createNewIndex(fitted_data)
1033        # Calculate difference between return_data and logic.data
1034        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
1035        # Update the control
1036        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
1037        self.lblChi2Value.setText(chi2_repr)
1038
1039        self.communicate.plotUpdateSignal.emit([fitted_data])
1040
1041        # Plot residuals if actual data
1042        if self.data_is_loaded:
1043            residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
1044            residuals_plot.id = "Residual " + residuals_plot.id
1045            self.createNewIndex(residuals_plot)
1046            self.communicate.plotUpdateSignal.emit([residuals_plot])
1047
1048    def calcException(self, etype, value, tb):
1049        """
1050        Something horrible happened in the deferred.
1051        """
1052        logging.error("".join(traceback.format_exception(etype, value, tb)))
1053
1054    def setTableProperties(self, table):
1055        """
1056        Setting table properties
1057        """
1058        # Table properties
1059        table.verticalHeader().setVisible(False)
1060        table.setAlternatingRowColors(True)
1061        table.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
1062        table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
1063        table.resizeColumnsToContents()
1064
1065        # Header
1066        header = table.horizontalHeader()
1067        header.setResizeMode(QtGui.QHeaderView.ResizeToContents)
1068
1069        header.ResizeMode(QtGui.QHeaderView.Interactive)
1070        # Resize column 0 and 6 to content
1071        header.setResizeMode(0, QtGui.QHeaderView.ResizeToContents)
1072        header.setResizeMode(6, QtGui.QHeaderView.ResizeToContents)
1073
1074    def setPolyModel(self):
1075        """
1076        Set polydispersity values
1077        """
1078        if not self.model_parameters:
1079            return
1080        self._poly_model.clear()
1081        for row, param in enumerate(self.model_parameters.form_volume_parameters):
1082            # Counters should not be included
1083            if not param.polydisperse:
1084                continue
1085
1086            # Potential multishell params
1087            checked_list = ["Distribution of "+param.name, str(param.default),
1088                            str(param.limits[0]), str(param.limits[1]),
1089                            "35", "3", "gaussian"]
1090            FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
1091
1092            #TODO: Need to find cleaner way to input functions
1093            #func = QtGui.QComboBox()
1094            #func.addItems(['rectangle', 'array', 'lognormal', 'gaussian', 'schulz',])
1095            #func_index = self.lstPoly.model().index(row, 6)
1096            #self.lstPoly.setIndexWidget(func_index, func)
1097
1098        FittingUtilities.addPolyHeadersToModel(self._poly_model)
1099
1100    def setMagneticModel(self):
1101        """
1102        Set magnetism values on model
1103        """
1104        if not self.model_parameters:
1105            return
1106        self._magnet_model.clear()
1107        for param in self.model_parameters.call_parameters:
1108            if param.type != "magnetic":
1109                continue
1110            checked_list = [param.name,
1111                            str(param.default),
1112                            str(param.limits[0]),
1113                            str(param.limits[1]),
1114                            param.units]
1115            FittingUtilities.addCheckedListToModel(self._magnet_model, checked_list)
1116
1117        FittingUtilities.addHeadersToModel(self._magnet_model)
1118
1119    def addStructureFactor(self):
1120        """
1121        Add structure factors to the list of parameters
1122        """
1123        if self.kernel_module.is_form_factor:
1124            self.enableStructureCombo()
1125        else:
1126            self.disableStructureCombo()
1127
1128    def addExtraShells(self):
1129        """
1130        Add a combobox for multiple shell display
1131        """
1132        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
1133
1134        if param_length == 0:
1135            return
1136
1137        # cell 1: variable name
1138        item1 = QtGui.QStandardItem(param_name)
1139
1140        func = QtGui.QComboBox()
1141        # Available range of shells displayed in the combobox
1142        func.addItems([str(i) for i in xrange(param_length+1)])
1143
1144        # Respond to index change
1145        func.currentIndexChanged.connect(self.modifyShellsInList)
1146
1147        # cell 2: combobox
1148        item2 = QtGui.QStandardItem()
1149        self._model_model.appendRow([item1, item2])
1150
1151        # Beautify the row:  span columns 2-4
1152        shell_row = self._model_model.rowCount()
1153        shell_index = self._model_model.index(shell_row-1, 1)
1154
1155        self.lstParams.setIndexWidget(shell_index, func)
1156        self._last_model_row = self._model_model.rowCount()
1157
1158        # Set the index to the state-kept value
1159        func.setCurrentIndex(self.current_shell_displayed
1160                             if self.current_shell_displayed < func.count() else 0)
1161
1162    def modifyShellsInList(self, index):
1163        """
1164        Add/remove additional multishell parameters
1165        """
1166        # Find row location of the combobox
1167        last_row = self._last_model_row
1168        remove_rows = self._model_model.rowCount() - last_row
1169
1170        if remove_rows > 1:
1171            self._model_model.removeRows(last_row, remove_rows)
1172
1173        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
1174        self.current_shell_displayed = index
1175
1176    def readFitPage(self, fp):
1177        """
1178        Read in state from a fitpage object and update GUI
1179        """
1180        assert isinstance(fp, FitPage)
1181        # Main tab info
1182        self.logic.data.filename = fp.filename
1183        self.data_is_loaded = fp.data_is_loaded
1184        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
1185        self.chkMagnetism.setCheckState(fp.is_magnetic)
1186        self.chk2DView.setCheckState(fp.is2D)
1187
1188        # Update the comboboxes
1189        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
1190        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
1191        if fp.current_factor:
1192            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
1193
1194        self.chi2 = fp.chi2
1195
1196        # Options tab
1197        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
1198        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
1199        self.npts = fp.fit_options[fp.NPTS]
1200        self.log_points = fp.fit_options[fp.LOG_POINTS]
1201        self.weighting = fp.fit_options[fp.WEIGHTING]
1202
1203        # Models
1204        self._model_model = fp.model_model
1205        self._poly_model = fp.poly_model
1206        self._magnet_model = fp.magnetism_model
1207
1208        # Resolution tab
1209        smearing = fp.smearing_options[fp.SMEARING_OPTION]
1210        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
1211        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
1212        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
1213        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
1214
1215        # TODO: add polidyspersity and magnetism
1216
1217    def saveToFitPage(self, fp):
1218        """
1219        Write current state to the given fitpage
1220        """
1221        assert isinstance(fp, FitPage)
1222
1223        # Main tab info
1224        fp.filename = self.logic.data.filename
1225        fp.data_is_loaded = self.data_is_loaded
1226        fp.is_polydisperse = self.chkPolydispersity.isChecked()
1227        fp.is_magnetic = self.chkMagnetism.isChecked()
1228        fp.is2D = self.chk2DView.isChecked()
1229        fp.data = self.data
1230
1231        # Use current models - they contain all the required parameters
1232        fp.model_model = self._model_model
1233        fp.poly_model = self._poly_model
1234        fp.magnetism_model = self._magnet_model
1235
1236        if self.cbCategory.currentIndex() != 0:
1237            fp.current_category = str(self.cbCategory.currentText())
1238            fp.current_model = str(self.cbModel.currentText())
1239
1240        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
1241            fp.current_factor = str(self.cbStructureFactor.currentText())
1242        else:
1243            fp.current_factor = ''
1244
1245        fp.chi2 = self.chi2
1246        fp.parameters_to_fit = self.parameters_to_fit
1247
1248        # Options tab
1249        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
1250        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
1251        fp.fit_options[fp.NPTS] = self.npts
1252        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
1253        fp.fit_options[fp.LOG_POINTS] = self.log_points
1254        fp.fit_options[fp.WEIGHTING] = self.weighting
1255
1256        # Resolution tab
1257        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
1258        fp.smearing_options[fp.SMEARING_OPTION] = smearing
1259        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
1260        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
1261        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
1262
1263        # TODO: add polidyspersity and magnetism
1264
1265
1266    def updateUndo(self):
1267        """
1268        Create a new state page and add it to the stack
1269        """
1270        if self.undo_supported:
1271            self.pushFitPage(self.currentState())
1272
1273    def currentState(self):
1274        """
1275        Return fit page with current state
1276        """
1277        new_page = FitPage()
1278        self.saveToFitPage(new_page)
1279
1280        return new_page
1281
1282    def pushFitPage(self, new_page):
1283        """
1284        Add a new fit page object with current state
1285        """
1286        self.page_stack.append(new_page)
1287        pass
1288
1289    def popFitPage(self):
1290        """
1291        Remove top fit page from stack
1292        """
1293        if self.page_stack:
1294            self.page_stack.pop()
1295        pass
1296
Note: See TracBrowser for help on using the repository browser.