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

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 9867860 was 9867860, checked in by wojciech, 7 years ago

Merge branch 'ESS_GUI' of https://github.com/SasView/sasview into ESS_GUI_magnetism_bug

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