source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @ 0d13814

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

SASVIEW-627 Fixed multishell parameters in all models/views

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