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

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

Fixes for structure factor chooser + minor refactoring in the FittingWidget?

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