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

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

Polydispersity support: cleanup and unit tests SASVIEW-575

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