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

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

Code review changes SASVIEW-597

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