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

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

Allow old style threads for fitting

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