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

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

Additional guard against unwanted GUI options on data load.

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