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

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

Added tab-dependent help display in fiting

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