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

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

Code review comments for SASVIEW-586

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