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

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

Fixed main model update on polydisp. parameter change, code cleanup, minor refactoring

  • Property mode set to 100644
File size: 58.7 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            property_name = str(self._poly_model.headerData(model_column, 1).toPyObject()).lower() # Value, min, max, etc.
504            # print "%s(%s) => %d" % (parameter_name, property_name, value)
505            current_details = self.kernel_module.details[parameter_name]
506            current_details[model_column-1] = value
507        elif model_column == self.lstPoly.itemDelegate().POLY_FUNCTION:
508            # name of the function - just pass
509            return
510        else:
511            try:
512                value = float(item.text())
513            except ValueError:
514                # Can't be converted properly, bring back the old value and exit
515                return
516            property_name = str(self._poly_model.headerData(model_column, 1).toPyObject()).lower() # Value, min, max, etc.
517
518            # Update the sasmodel
519            # PD[ratio] -> width, npts -> npts, nsigs -> nsigmas
520            self.kernel_module.setParam(parameter_name + '.' + \
521                                        self.lstPoly.itemDelegate().POLY_COLUMN_DICT[model_column], value)
522
523    def onHelp(self):
524        """
525        Show the "Fitting" section of help
526        """
527        tree_location = self.parent.HELP_DIRECTORY_LOCATION + "/user/sasgui/perspectives/fitting/"
528
529        # Actual file will depend on the current tab
530        tab_id = self.tabFitting.currentIndex()
531        helpfile = "fitting.html"
532        if tab_id == 0:
533            helpfile = "fitting_help.html"
534        elif tab_id == 1:
535            helpfile = "residuals_help.html"
536        elif tab_id == 2:
537            helpfile = "sm_help.html"
538        elif tab_id == 3:
539            helpfile = "pd_help.html"
540        elif tab_id == 4:
541            helpfile = "mag_help.html"
542        help_location = tree_location + helpfile
543        self.helpView.load(QtCore.QUrl(help_location))
544        self.helpView.show()
545
546    def onFit(self):
547        """
548        Perform fitting on the current data
549        """
550        fitter = Fit()
551
552        # Data going in
553        data = self.logic.data
554        model = self.kernel_module
555        qmin = self.q_range_min
556        qmax = self.q_range_max
557        params_to_fit = self.parameters_to_fit
558
559        # Potential weights added directly to data
560        self.addWeightingToData(data)
561
562        # Potential smearing added
563        # Remember that smearing_min/max can be None ->
564        # deal with it until Python gets discriminated unions
565        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
566
567        # These should be updating somehow?
568        fit_id = 0
569        constraints = []
570        smearer = None
571        page_id = [210]
572        handler = None
573        batch_inputs = {}
574        batch_outputs = {}
575        list_page_id = [page_id]
576        #---------------------------------
577
578        # Parameterize the fitter
579        fitter.set_model(model, fit_id, params_to_fit, data=data,
580                         constraints=constraints)
581
582        fitter.set_data(data=data, id=fit_id, smearer=smearer, qmin=qmin,
583                        qmax=qmax)
584        fitter.select_problem_for_fit(id=fit_id, value=1)
585
586        fitter.fitter_id = page_id
587
588        # Create the fitting thread, based on the fitter
589        calc_fit = FitThread(handler=handler,
590                             fn=[fitter],
591                             batch_inputs=batch_inputs,
592                             batch_outputs=batch_outputs,
593                             page_id=list_page_id,
594                             updatefn=self.updateFit,
595                             completefn=None)
596
597        # start the trhrhread
598        calc_thread = threads.deferToThread(calc_fit.compute)
599        calc_thread.addCallback(self.fitComplete)
600        calc_thread.addErrback(self.fitFailed)
601
602        #disable the Fit button
603        self.cmdFit.setText('Calculating...')
604        self.communicate.statusBarUpdateSignal.emit('Fitting started...')
605        self.cmdFit.setEnabled(False)
606
607    def updateFit(self):
608        """
609        """
610        print "UPDATE FIT"
611        pass
612
613    def fitFailed(self, reason):
614        """
615        """
616        print "FIT FAILED: ", reason
617        pass
618
619    def fitComplete(self, result):
620        """
621        Receive and display fitting results
622        "result" is a tuple of actual result list and the fit time in seconds
623        """
624        #re-enable the Fit button
625        self.cmdFit.setText("Fit")
626        self.cmdFit.setEnabled(True)
627
628        assert result is not None
629
630        res_list = result[0]
631        res = res_list[0]
632        if res.fitness is None or \
633            not np.isfinite(res.fitness) or \
634            np.any(res.pvec is None) or \
635            not np.all(np.isfinite(res.pvec)):
636            msg = "Fitting did not converge!!!"
637            self.communicate.statusBarUpdateSignal.emit(msg)
638            logging.error(msg)
639            return
640
641        elapsed = result[1]
642        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
643        self.communicate.statusBarUpdateSignal.emit(msg)
644
645        self.chi2 = res.fitness
646        param_list = res.param_list # ['radius', 'radius.width']
647        param_values = res.pvec     # array([ 0.36221662,  0.0146783 ])
648        param_stderr = res.stderr   # array([ 1.71293015,  1.71294233])
649        params_and_errors = zip(param_values, param_stderr)
650        param_dict = dict(izip(param_list, params_and_errors))
651
652        # Dictionary of fitted parameter: value, error
653        # e.g. param_dic = {"sld":(1.703, 0.0034), "length":(33.455, -0.0983)}
654        self.updateModelFromList(param_dict)
655
656        self.updatePolyModelFromList(param_dict)
657
658        # update charts
659        self.onPlot()
660
661        # Read only value - we can get away by just printing it here
662        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
663        self.lblChi2Value.setText(chi2_repr)
664
665    def iterateOverModel(self, func):
666        """
667        Take func and throw it inside the model row loop
668        """
669        for row_i in xrange(self._model_model.rowCount()):
670            func(row_i)
671
672    def updateModelFromList(self, param_dict):
673        """
674        Update the model with new parameters, create the errors column
675        """
676        assert isinstance(param_dict, dict)
677        if not dict:
678            return
679
680        def updateFittedValues(row):
681            # Utility function for main model update
682            # internal so can use closure for param_dict
683            param_name = str(self._model_model.item(row, 0).text())
684            if param_name not in param_dict.keys():
685                return
686            # modify the param value
687            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
688            self._model_model.item(row, 1).setText(param_repr)
689            if self.has_error_column:
690                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
691                self._model_model.item(row, 2).setText(error_repr)
692
693        def updatePolyValues(row):
694            # Utility function for updateof polydispersity part of the main model
695            param_name = str(self._model_model.item(row, 0).text())+'.width'
696            if param_name not in param_dict.keys():
697                return
698            # modify the param value
699            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
700            self._model_model.item(row, 0).child(0).child(0,1).setText(param_repr)
701
702        def createErrorColumn(row):
703            # Utility function for error column update
704            item = QtGui.QStandardItem()
705            def createItem(param_name):
706                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
707                item.setText(error_repr)
708            def curr_param():
709                return str(self._model_model.item(row, 0).text())
710
711            [createItem(param_name) for param_name in param_dict.keys() if curr_param() == param_name]
712
713            error_column.append(item)
714
715        # block signals temporarily, so we don't end up
716        # updating charts with every single model change on the end of fitting
717        self._model_model.blockSignals(True)
718        self.iterateOverModel(updateFittedValues)
719        self.iterateOverModel(updatePolyValues)
720        self._model_model.blockSignals(False)
721
722        if self.has_error_column:
723            return
724
725        error_column = []
726        self.iterateOverModel(createErrorColumn)
727
728        # switch off reponse to model change
729        self._model_model.blockSignals(True)
730        self._model_model.insertColumn(2, error_column)
731        self._model_model.blockSignals(False)
732        FittingUtilities.addErrorHeadersToModel(self._model_model)
733        # Adjust the table cells width.
734        # TODO: find a way to dynamically adjust column width while resized expanding
735        self.lstParams.resizeColumnToContents(0)
736        self.lstParams.resizeColumnToContents(4)
737        self.lstParams.resizeColumnToContents(5)
738        self.lstParams.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
739
740        self.has_error_column = True
741
742    def updatePolyModelFromList(self, param_dict):
743        """
744        Update the polydispersity model with new parameters, create the errors column
745        """
746        assert isinstance(param_dict, dict)
747        if not dict:
748            return
749
750        def updateFittedValues(row_i):
751            # Utility function for main model update
752            # internal so can use closure for param_dict
753            if row_i >= self._poly_model.rowCount():
754                return
755            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
756            if param_name not in param_dict.keys():
757                return
758            # modify the param value
759            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
760            self._poly_model.item(row_i, 1).setText(param_repr)
761            if self.has_poly_error_column:
762                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
763                self._poly_model.item(row_i, 2).setText(error_repr)
764
765
766        def createErrorColumn(row_i):
767            # Utility function for error column update
768            if row_i >= self._poly_model.rowCount():
769                return
770            item = QtGui.QStandardItem()
771
772            def createItem(param_name):
773                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
774                item.setText(error_repr)
775
776            def poly_param():
777                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
778
779            [createItem(param_name) for param_name in param_dict.keys() if poly_param() == param_name]
780
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        if self.has_poly_error_column:
790            return
791
792        # Duck type delegate variables
793        self.lstPoly.itemDelegate().POLY_MIN = 3
794        self.lstPoly.itemDelegate().POLY_MAX = 4
795        self.lstPoly.itemDelegate().POLY_NPTS = 5
796        self.lstPoly.itemDelegate().POLY_NSIGS = 6
797        self.lstPoly.itemDelegate().POLY_FUNCTION = 7
798        self.lstPoly.itemDelegate().POLY_COLUMN_DICT = {
799            2: 'width',
800            3: 'min',
801            4: 'max',
802            5: 'npts',
803            6: 'nsigmas'}
804
805        error_column = []
806        self.iterateOverModel(createErrorColumn)
807
808        # switch off reponse to model change
809        self._poly_model.blockSignals(True)
810        self._poly_model.insertColumn(2, error_column)
811        self._poly_model.blockSignals(False)
812        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
813
814        self.has_poly_error_column = True
815
816    def onPlot(self):
817        """
818        Plot the current set of data
819        """
820        # Regardless of previous state, this should now be `plot show` functionality only
821        self.cmdPlot.setText("Show Plot")
822        if not self.data_is_loaded:
823            self.recalculatePlotData()
824        self.showPlot()
825
826    def recalculatePlotData(self):
827        """
828        Generate a new dataset for model
829        """
830        if not self.data_is_loaded:
831            self.createDefaultDataset()
832        self.calculateQGridForModel()
833
834    def showPlot(self):
835        """
836        Show the current plot in MPL
837        """
838        # Show the chart if ready
839        data_to_show = self.data if self.data_is_loaded else self.model_data
840        if data_to_show is not None:
841            self.communicate.plotRequestedSignal.emit([data_to_show])
842
843    def onOptionsUpdate(self):
844        """
845        Update local option values and replot
846        """
847        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
848            self.options_widget.state()
849        # set Q range labels on the main tab
850        self.lblMinRangeDef.setText(str(self.q_range_min))
851        self.lblMaxRangeDef.setText(str(self.q_range_max))
852        self.recalculatePlotData()
853
854    def setDefaultStructureCombo(self):
855        """
856        Fill in the structure factors combo box with defaults
857        """
858        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
859        factors = [factor[0] for factor in structure_factor_list]
860        factors.insert(0, STRUCTURE_DEFAULT)
861        self.cbStructureFactor.clear()
862        self.cbStructureFactor.addItems(sorted(factors))
863
864    def createDefaultDataset(self):
865        """
866        Generate default Dataset 1D/2D for the given model
867        """
868        # Create default datasets if no data passed
869        if self.is2D:
870            qmax = self.q_range_max/np.sqrt(2)
871            qstep = self.npts
872            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
873            return
874        elif self.log_points:
875            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
876            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
877            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
878        else:
879            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
880                                   num=self.npts, endpoint=True)
881        self.logic.createDefault1dData(interval, self.tab_id)
882
883    def readCategoryInfo(self):
884        """
885        Reads the categories in from file
886        """
887        self.master_category_dict = defaultdict(list)
888        self.by_model_dict = defaultdict(list)
889        self.model_enabled_dict = defaultdict(bool)
890
891        categorization_file = CategoryInstaller.get_user_file()
892        if not os.path.isfile(categorization_file):
893            categorization_file = CategoryInstaller.get_default_file()
894        with open(categorization_file, 'rb') as cat_file:
895            self.master_category_dict = json.load(cat_file)
896            self.regenerateModelDict()
897
898        # Load the model dict
899        models = load_standard_models()
900        for model in models:
901            self.models[model.name] = model
902
903    def regenerateModelDict(self):
904        """
905        Regenerates self.by_model_dict which has each model name as the
906        key and the list of categories belonging to that model
907        along with the enabled mapping
908        """
909        self.by_model_dict = defaultdict(list)
910        for category in self.master_category_dict:
911            for (model, enabled) in self.master_category_dict[category]:
912                self.by_model_dict[model].append(category)
913                self.model_enabled_dict[model] = enabled
914
915    def addBackgroundToModel(self, model):
916        """
917        Adds background parameter with default values to the model
918        """
919        assert isinstance(model, QtGui.QStandardItemModel)
920        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
921        FittingUtilities.addCheckedListToModel(model, checked_list)
922        last_row = model.rowCount()-1
923        model.item(last_row, 0).setEditable(False)
924        model.item(last_row, 4).setEditable(False)
925
926    def addScaleToModel(self, model):
927        """
928        Adds scale parameter with default values to the model
929        """
930        assert isinstance(model, QtGui.QStandardItemModel)
931        checked_list = ['scale', '1.0', '0.0', 'inf', '']
932        FittingUtilities.addCheckedListToModel(model, checked_list)
933        last_row = model.rowCount()-1
934        model.item(last_row, 0).setEditable(False)
935        model.item(last_row, 4).setEditable(False)
936
937    def addWeightingToData(self, data):
938        """
939        Adds weighting contribution to fitting data
940        """
941        # Send original data for weighting
942        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
943        update_module = data.err_data if self.is2D else data.dy
944        # Overwrite relevant values in data
945        update_module = weight
946
947    def updateQRange(self):
948        """
949        Updates Q Range display
950        """
951        if self.data_is_loaded:
952            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
953        # set Q range labels on the main tab
954        self.lblMinRangeDef.setText(str(self.q_range_min))
955        self.lblMaxRangeDef.setText(str(self.q_range_max))
956        # set Q range labels on the options tab
957        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
958
959    def SASModelToQModel(self, model_name, structure_factor=None):
960        """
961        Setting model parameters into table based on selected category
962        """
963        # Crete/overwrite model items
964        self._model_model.clear()
965
966        # First, add parameters from the main model
967        if model_name is not None:
968            self.fromModelToQModel(model_name)
969
970        # Then, add structure factor derived parameters
971        if structure_factor is not None and structure_factor != "None":
972            if model_name is None:
973                # Instantiate the current sasmodel for SF-only models
974                self.kernel_module = self.models[structure_factor]()
975            self.fromStructureFactorToQModel(structure_factor)
976        else:
977            # Allow the SF combobox visibility for the given sasmodel
978            self.enableStructureFactorControl(structure_factor)
979
980        # Then, add multishells
981        if model_name is not None:
982            # Multishell models need additional treatment
983            self.addExtraShells()
984
985        # Add polydispersity to the model
986        self.setPolyModel()
987        # Add magnetic parameters to the model
988        self.setMagneticModel()
989
990        # Adjust the table cells width
991        self.lstParams.resizeColumnToContents(0)
992        self.lstParams.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
993
994        # Now we claim the model has been loaded
995        self.model_is_loaded = True
996
997        # (Re)-create headers
998        FittingUtilities.addHeadersToModel(self._model_model)
999        self.lstParams.header().setFont(self.boldFont)
1000
1001        # Update Q Ranges
1002        self.updateQRange()
1003
1004    def fromModelToQModel(self, model_name):
1005        """
1006        Setting model parameters into QStandardItemModel based on selected _model_
1007        """
1008        kernel_module = generate.load_kernel_module(model_name)
1009        self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
1010
1011        # Instantiate the current sasmodel
1012        self.kernel_module = self.models[model_name]()
1013
1014        # Explicitly add scale and background with default values
1015        temp_undo_state = self.undo_supported
1016        self.undo_supported = False
1017        self.addScaleToModel(self._model_model)
1018        self.addBackgroundToModel(self._model_model)
1019        self.undo_supported = temp_undo_state
1020
1021        # Update the QModel
1022        new_rows = FittingUtilities.addParametersToModel(self.model_parameters, self.kernel_module, self.is2D)
1023
1024        for row in new_rows:
1025            self._model_model.appendRow(row)
1026        # Update the counter used for multishell display
1027        self._last_model_row = self._model_model.rowCount()
1028
1029    def fromStructureFactorToQModel(self, structure_factor):
1030        """
1031        Setting model parameters into QStandardItemModel based on selected _structure factor_
1032        """
1033        structure_module = generate.load_kernel_module(structure_factor)
1034        structure_parameters = modelinfo.make_parameter_table(getattr(structure_module, 'parameters', []))
1035
1036        new_rows = FittingUtilities.addSimpleParametersToModel(structure_parameters, self.is2D)
1037        for row in new_rows:
1038            self._model_model.appendRow(row)
1039        # Update the counter used for multishell display
1040        self._last_model_row = self._model_model.rowCount()
1041
1042    def updateParamsFromModel(self, item):
1043        """
1044        Callback method for updating the sasmodel parameters with the GUI values
1045        """
1046        model_column = item.column()
1047
1048        if model_column == 0:
1049            self.checkboxSelected(item)
1050            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
1051            # Update state stack
1052            self.updateUndo()
1053            return
1054
1055        model_row = item.row()
1056        name_index = self._model_model.index(model_row, 0)
1057
1058        # Extract changed value. Assumes proper validation by QValidator/Delegate
1059        try:
1060            value = float(item.text())
1061        except ValueError:
1062            # Unparsable field
1063            return
1064        parameter_name = str(self._model_model.data(name_index).toPyObject()) # sld, background etc.
1065        property_index = self._model_model.headerData(1, model_column).toInt()[0]-1 # Value, min, max, etc.
1066
1067        # Update the parameter value - note: this supports +/-inf as well
1068        self.kernel_module.params[parameter_name] = value
1069
1070        # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1071        self.kernel_module.details[parameter_name][property_index] = value
1072
1073        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
1074        # TODO: multishell params in self.kernel_module.details[??] = value
1075
1076        # Force the chart update when actual parameters changed
1077        if model_column == 1:
1078            self.recalculatePlotData()
1079
1080        # Update state stack
1081        self.updateUndo()
1082
1083    def checkboxSelected(self, item):
1084        # Assure we're dealing with checkboxes
1085        if not item.isCheckable():
1086            return
1087        status = item.checkState()
1088
1089        def isCheckable(row):
1090            return self._model_model.item(row, 0).isCheckable()
1091
1092        # If multiple rows selected - toggle all of them, filtering uncheckable
1093        rows = [s.row() for s in self.lstParams.selectionModel().selectedRows() if isCheckable(s.row())]
1094
1095        # Switch off signaling from the model to avoid recursion
1096        self._model_model.blockSignals(True)
1097        # Convert to proper indices and set requested enablement
1098        [self._model_model.item(row, 0).setCheckState(status) for row in rows]
1099        self._model_model.blockSignals(False)
1100
1101        # update the list of parameters to fit
1102        main_params = self.checkedListFromModel(self._model_model)
1103        poly_params = self.checkedListFromModel(self._poly_model)
1104        # Retrieve poly params names
1105        poly_params = [param.rsplit()[-1] + '.width' for param in poly_params]
1106        # TODO : add magnetic params
1107
1108        self.parameters_to_fit = main_params + poly_params
1109
1110    def checkedListFromModel(self, model):
1111        """
1112        Returns list of checked parameters for given model
1113        """
1114        def isChecked(row):
1115            return model.item(row, 0).checkState() == QtCore.Qt.Checked
1116
1117        return [str(model.item(row_index, 0).text())
1118                for row_index in xrange(model.rowCount())
1119                if isChecked(row_index)]
1120
1121    def nameForFittedData(self, name):
1122        """
1123        Generate name for the current fit
1124        """
1125        if self.is2D:
1126            name += "2d"
1127        name = "M%i [%s]" % (self.tab_id, name)
1128        return name
1129
1130    def createNewIndex(self, fitted_data):
1131        """
1132        Create a model or theory index with passed Data1D/Data2D
1133        """
1134        if self.data_is_loaded:
1135            if not fitted_data.name:
1136                name = self.nameForFittedData(self.data.filename)
1137                fitted_data.title = name
1138                fitted_data.name = name
1139                fitted_data.filename = name
1140                fitted_data.symbol = "Line"
1141            self.updateModelIndex(fitted_data)
1142        else:
1143            name = self.nameForFittedData(self.kernel_module.name)
1144            fitted_data.title = name
1145            fitted_data.name = name
1146            fitted_data.filename = name
1147            fitted_data.symbol = "Line"
1148            self.createTheoryIndex(fitted_data)
1149
1150    def updateModelIndex(self, fitted_data):
1151        """
1152        Update a QStandardModelIndex containing model data
1153        """
1154        name = self.nameFromData(fitted_data)
1155        # Make this a line if no other defined
1156        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
1157            fitted_data.symbol = 'Line'
1158        # Notify the GUI manager so it can update the main model in DataExplorer
1159        GuiUtils.updateModelItemWithPlot(self._index, QtCore.QVariant(fitted_data), name)
1160
1161    def createTheoryIndex(self, fitted_data):
1162        """
1163        Create a QStandardModelIndex containing model data
1164        """
1165        name = self.nameFromData(fitted_data)
1166        # Notify the GUI manager so it can create the theory model in DataExplorer
1167        new_item = GuiUtils.createModelItemWithPlot(QtCore.QVariant(fitted_data), name=name)
1168        self.communicate.updateTheoryFromPerspectiveSignal.emit(new_item)
1169
1170    def nameFromData(self, fitted_data):
1171        """
1172        Return name for the dataset. Terribly impure function.
1173        """
1174        if fitted_data.name is None:
1175            name = self.nameForFittedData(self.logic.data.filename)
1176            fitted_data.title = name
1177            fitted_data.name = name
1178            fitted_data.filename = name
1179        else:
1180            name = fitted_data.name
1181        return name
1182
1183    def methodCalculateForData(self):
1184        '''return the method for data calculation'''
1185        return Calc1D if isinstance(self.data, Data1D) else Calc2D
1186
1187    def methodCompleteForData(self):
1188        '''return the method for result parsin on calc complete '''
1189        return self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1190
1191    def calculateQGridForModel(self):
1192        """
1193        Prepare the fitting data object, based on current ModelModel
1194        """
1195        if self.kernel_module is None:
1196            return
1197        # Awful API to a backend method.
1198        method = self.methodCalculateForData()(data=self.data,
1199                                               model=self.kernel_module,
1200                                               page_id=0,
1201                                               qmin=self.q_range_min,
1202                                               qmax=self.q_range_max,
1203                                               smearer=None,
1204                                               state=None,
1205                                               weight=None,
1206                                               fid=None,
1207                                               toggle_mode_on=False,
1208                                               completefn=None,
1209                                               update_chisqr=True,
1210                                               exception_handler=self.calcException,
1211                                               source=None)
1212
1213        calc_thread = threads.deferToThread(method.compute)
1214        calc_thread.addCallback(self.methodCompleteForData())
1215        calc_thread.addErrback(self.calculateDataFailed)
1216
1217    def calculateDataFailed(self, reason):
1218        """
1219        Thread returned error
1220        """
1221        print "Calculate Data failed with ", reason
1222
1223    def complete1D(self, return_data):
1224        """
1225        Plot the current 1D data
1226        """
1227        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
1228        self.calculateResiduals(fitted_data)
1229        self.model_data = fitted_data
1230
1231    def complete2D(self, return_data):
1232        """
1233        Plot the current 2D data
1234        """
1235        fitted_data = self.logic.new2DPlot(return_data)
1236        self.calculateResiduals(fitted_data)
1237        self.model_data = fitted_data
1238
1239    def calculateResiduals(self, fitted_data):
1240        """
1241        Calculate and print Chi2 and display chart of residuals
1242        """
1243        # Create a new index for holding data
1244        fitted_data.symbol = "Line"
1245
1246        # Modify fitted_data with weighting
1247        self.addWeightingToData(fitted_data)
1248
1249        self.createNewIndex(fitted_data)
1250        # Calculate difference between return_data and logic.data
1251        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
1252        # Update the control
1253        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
1254        self.lblChi2Value.setText(chi2_repr)
1255
1256        self.communicate.plotUpdateSignal.emit([fitted_data])
1257
1258        # Plot residuals if actual data
1259        if not self.data_is_loaded:
1260            return
1261
1262        residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
1263        residuals_plot.id = "Residual " + residuals_plot.id
1264        self.createNewIndex(residuals_plot)
1265        self.communicate.plotUpdateSignal.emit([residuals_plot])
1266
1267    def calcException(self, etype, value, tb):
1268        """
1269        Thread threw an exception.
1270        """
1271        # TODO: remimplement thread cancellation
1272        logging.error("".join(traceback.format_exception(etype, value, tb)))
1273
1274    def setTableProperties(self, table):
1275        """
1276        Setting table properties
1277        """
1278        # Table properties
1279        table.verticalHeader().setVisible(False)
1280        table.setAlternatingRowColors(True)
1281        table.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
1282        table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
1283        table.resizeColumnsToContents()
1284
1285        # Header
1286        header = table.horizontalHeader()
1287        header.setResizeMode(QtGui.QHeaderView.ResizeToContents)
1288
1289        header.ResizeMode(QtGui.QHeaderView.Interactive)
1290        # Resize column 0 and 6 to content
1291        header.setResizeMode(0, QtGui.QHeaderView.ResizeToContents)
1292        header.setResizeMode(6, QtGui.QHeaderView.ResizeToContents)
1293
1294    def setPolyModel(self):
1295        """
1296        Set polydispersity values
1297        """
1298        if not self.model_parameters:
1299            return
1300        self._poly_model.clear()
1301
1302        [self.setPolyModelParameters(row, param) for row, param in \
1303            enumerate(self.model_parameters.form_volume_parameters) if param.polydisperse]
1304        FittingUtilities.addPolyHeadersToModel(self._poly_model)
1305
1306    def setPolyModelParameters(self, row, param):
1307        """
1308        Creates a checked row of for a polydisperse parameter
1309        """
1310        # Not suitable for multishell
1311        if '[' in param.name:
1312            return
1313        # Values from the sasmodel
1314        width = self.kernel_module.getParam(param.name+'.width')
1315        npts = self.kernel_module.getParam(param.name+'.npts')
1316        nsigs = self.kernel_module.getParam(param.name+'.nsigmas')
1317        # Potential multishell params
1318        checked_list = ["Distribution of "+param.name, str(width),
1319                        str(param.limits[0]), str(param.limits[1]),
1320                        str(npts), str(nsigs), "gaussian      "]
1321        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
1322
1323        # All possible polydisp. functions as strings in combobox
1324        func = QtGui.QComboBox()
1325        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.iterkeys()])
1326        # Default index
1327        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
1328        # Index in the view
1329        #func_index = self.lstPoly.model().index(row, 6)
1330
1331    def onPolyComboIndexChange(self, combo_string, row_index):
1332        """
1333        Modify polydisp. defaults on function choice
1334        """
1335        # get npts/nsigs for current selection
1336        param = self.model_parameters.form_volume_parameters[row_index]
1337
1338        def updateFunctionCaption(row):
1339            # Utility function for update of polydispersity function name in the main model
1340            param_name = str(self._model_model.item(row, 0).text())# +'.width'
1341            if param_name !=  param.name:
1342                return
1343            # modify the param value
1344            self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
1345
1346        if combo_string == 'array':
1347            try:
1348                self.loadPolydispArray()
1349                # Update main model for display
1350                self.iterateOverModel(updateFunctionCaption)
1351            except (ValueError, IOError):
1352                # Don't do anything if file lookup failed
1353                return
1354            # disable the row
1355            [self._poly_model.item(row_index, i).setEnabled(False) for i in xrange(6)]
1356            return
1357
1358        # Enable the row in case it was disabled by Array
1359        self._poly_model.blockSignals(True)
1360        [self._poly_model.item(row_index, i).setEnabled(True) for i in xrange(6)]
1361        self._poly_model.blockSignals(False)
1362
1363        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().POLY_NPTS)
1364        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().POLY_NSIGS)
1365
1366        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
1367        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
1368
1369        self._poly_model.setData(npts_index, QtCore.QVariant(npts))
1370        self._poly_model.setData(nsigs_index, QtCore.QVariant(nsigs))
1371
1372        self.iterateOverModel(updateFunctionCaption)
1373
1374    def loadPolydispArray(self):
1375        """
1376        Show the load file dialog and loads requested data into state
1377        """
1378        datafile = QtGui.QFileDialog.getOpenFileName(
1379            self, "Choose a weight file", "", "All files (*.*)")
1380        if not datafile:
1381            logging.info("No weight data chosen.")
1382            return
1383        values = []
1384        weights = []
1385        def appendData(data_tuple):
1386            """
1387            Fish out floats from a tuple of strings
1388            """
1389            try:
1390                values.append(float(data_tuple[0]))
1391                weights.append(float(data_tuple[1]))
1392            except (ValueError, IndexError):
1393                # just pass through if line with bad data
1394                return
1395
1396        with open(datafile, 'r') as column_file:
1397            column_data = [line.rstrip().split() for line in column_file.readlines()]
1398            [appendData(line) for line in column_data]
1399
1400        self.disp_model = POLYDISPERSITY_MODELS['array']()
1401        self.disp_model.set_weights(np.array(values), np.array(weights))
1402
1403    def setMagneticModel(self):
1404        """
1405        Set magnetism values on model
1406        """
1407        if not self.model_parameters:
1408            return
1409        self._magnet_model.clear()
1410        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
1411            self.model_parameters.call_parameters if param.type == 'magnetic']
1412        FittingUtilities.addHeadersToModel(self._magnet_model)
1413
1414    def addCheckedMagneticListToModel(self, param, model):
1415        """
1416        Wrapper for model update with a subset of magnetic parameters
1417        """
1418        checked_list = [param.name,
1419                        str(param.default),
1420                        str(param.limits[0]),
1421                        str(param.limits[1]),
1422                        param.units]
1423
1424        FittingUtilities.addCheckedListToModel(model, checked_list)
1425
1426    def enableStructureFactorControl(self, structure_factor):
1427        """
1428        Add structure factors to the list of parameters
1429        """
1430        if self.kernel_module.is_form_factor or structure_factor == 'None':
1431            self.enableStructureCombo()
1432        else:
1433            self.disableStructureCombo()
1434
1435    def addExtraShells(self):
1436        """
1437        Add a combobox for multiple shell display
1438        """
1439        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
1440
1441        if param_length == 0:
1442            return
1443
1444        # cell 1: variable name
1445        item1 = QtGui.QStandardItem(param_name)
1446
1447        func = QtGui.QComboBox()
1448        # Available range of shells displayed in the combobox
1449        func.addItems([str(i) for i in xrange(param_length+1)])
1450
1451        # Respond to index change
1452        func.currentIndexChanged.connect(self.modifyShellsInList)
1453
1454        # cell 2: combobox
1455        item2 = QtGui.QStandardItem()
1456        self._model_model.appendRow([item1, item2])
1457
1458        # Beautify the row:  span columns 2-4
1459        shell_row = self._model_model.rowCount()
1460        shell_index = self._model_model.index(shell_row-1, 1)
1461
1462        self.lstParams.setIndexWidget(shell_index, func)
1463        self._last_model_row = self._model_model.rowCount()
1464
1465        # Set the index to the state-kept value
1466        func.setCurrentIndex(self.current_shell_displayed
1467                             if self.current_shell_displayed < func.count() else 0)
1468
1469    def modifyShellsInList(self, index):
1470        """
1471        Add/remove additional multishell parameters
1472        """
1473        # Find row location of the combobox
1474        last_row = self._last_model_row
1475        remove_rows = self._model_model.rowCount() - last_row
1476
1477        if remove_rows > 1:
1478            self._model_model.removeRows(last_row, remove_rows)
1479
1480        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
1481        self.current_shell_displayed = index
1482
1483    def readFitPage(self, fp):
1484        """
1485        Read in state from a fitpage object and update GUI
1486        """
1487        assert isinstance(fp, FitPage)
1488        # Main tab info
1489        self.logic.data.filename = fp.filename
1490        self.data_is_loaded = fp.data_is_loaded
1491        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
1492        self.chkMagnetism.setCheckState(fp.is_magnetic)
1493        self.chk2DView.setCheckState(fp.is2D)
1494
1495        # Update the comboboxes
1496        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
1497        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
1498        if fp.current_factor:
1499            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
1500
1501        self.chi2 = fp.chi2
1502
1503        # Options tab
1504        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
1505        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
1506        self.npts = fp.fit_options[fp.NPTS]
1507        self.log_points = fp.fit_options[fp.LOG_POINTS]
1508        self.weighting = fp.fit_options[fp.WEIGHTING]
1509
1510        # Models
1511        self._model_model = fp.model_model
1512        self._poly_model = fp.poly_model
1513        self._magnet_model = fp.magnetism_model
1514
1515        # Resolution tab
1516        smearing = fp.smearing_options[fp.SMEARING_OPTION]
1517        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
1518        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
1519        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
1520        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
1521
1522        # TODO: add polidyspersity and magnetism
1523
1524    def saveToFitPage(self, fp):
1525        """
1526        Write current state to the given fitpage
1527        """
1528        assert isinstance(fp, FitPage)
1529
1530        # Main tab info
1531        fp.filename = self.logic.data.filename
1532        fp.data_is_loaded = self.data_is_loaded
1533        fp.is_polydisperse = self.chkPolydispersity.isChecked()
1534        fp.is_magnetic = self.chkMagnetism.isChecked()
1535        fp.is2D = self.chk2DView.isChecked()
1536        fp.data = self.data
1537
1538        # Use current models - they contain all the required parameters
1539        fp.model_model = self._model_model
1540        fp.poly_model = self._poly_model
1541        fp.magnetism_model = self._magnet_model
1542
1543        if self.cbCategory.currentIndex() != 0:
1544            fp.current_category = str(self.cbCategory.currentText())
1545            fp.current_model = str(self.cbModel.currentText())
1546
1547        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
1548            fp.current_factor = str(self.cbStructureFactor.currentText())
1549        else:
1550            fp.current_factor = ''
1551
1552        fp.chi2 = self.chi2
1553        fp.parameters_to_fit = self.parameters_to_fit
1554        fp.kernel_module = self.kernel_module
1555
1556        # Options tab
1557        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
1558        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
1559        fp.fit_options[fp.NPTS] = self.npts
1560        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
1561        fp.fit_options[fp.LOG_POINTS] = self.log_points
1562        fp.fit_options[fp.WEIGHTING] = self.weighting
1563
1564        # Resolution tab
1565        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
1566        fp.smearing_options[fp.SMEARING_OPTION] = smearing
1567        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
1568        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
1569        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
1570
1571        # TODO: add polidyspersity and magnetism
1572
1573
1574    def updateUndo(self):
1575        """
1576        Create a new state page and add it to the stack
1577        """
1578        if self.undo_supported:
1579            self.pushFitPage(self.currentState())
1580
1581    def currentState(self):
1582        """
1583        Return fit page with current state
1584        """
1585        new_page = FitPage()
1586        self.saveToFitPage(new_page)
1587
1588        return new_page
1589
1590    def pushFitPage(self, new_page):
1591        """
1592        Add a new fit page object with current state
1593        """
1594        self.page_stack.append(new_page)
1595
1596    def popFitPage(self):
1597        """
1598        Remove top fit page from stack
1599        """
1600        if self.page_stack:
1601            self.page_stack.pop()
1602
Note: See TracBrowser for help on using the repository browser.