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

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

Attempt to pass parameters

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