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

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 a14a2b0 was a14a2b0, checked in by celinedurniak <celine.durniak@…>, 7 years ago

Fixed bug with non updated value of fitted parameter in new GUI

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