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

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

Added descriptive tooltips to column headers

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