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

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

Magnetic angles image widget

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