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

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

Towards the FitPage? stack.
Improvements to view delegates.

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