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

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

SASVIEW-272: initial implementation of magnetic model/view

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