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

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

Merge remote-tracking branch 'origin/ESS_GUI_ToolTips' into ESS_GUI

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