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

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

Minor refactoring + improvements to fitting views

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