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

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

Removed qtgui dependency on sasgui and wx SASVIEW-590

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