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

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

Don't update parameter value on param range changes SASVIEW-676

  • Property mode set to 100644
File size: 70.4 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        # Update the parameter value - note: this supports +/-inf as well
1277        if model_column == 0:
1278            self.kernel_module.setParam(parameter_name, value)
1279        else:
1280            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1281            self.kernel_module.details[parameter_name][property_index] = value
1282
1283        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
1284        # TODO: multishell params in self.kernel_module.details[??] = value
1285
1286        # Force the chart update when actual parameters changed
1287        if model_column == 1:
1288            self.recalculatePlotData()
1289
1290        # Update state stack
1291        self.updateUndo()
1292
1293    def checkboxSelected(self, item):
1294        # Assure we're dealing with checkboxes
1295        if not item.isCheckable():
1296            return
1297        status = item.checkState()
1298
1299        def isCheckable(row):
1300            return self._model_model.item(row, 0).isCheckable()
1301
1302        # If multiple rows selected - toggle all of them, filtering uncheckable
1303        rows = [s.row() for s in self.lstParams.selectionModel().selectedRows() if isCheckable(s.row())]
1304
1305        # Switch off signaling from the model to avoid recursion
1306        self._model_model.blockSignals(True)
1307        # Convert to proper indices and set requested enablement
1308        [self._model_model.item(row, 0).setCheckState(status) for row in rows]
1309        self._model_model.blockSignals(False)
1310
1311        # update the list of parameters to fit
1312        main_params = self.checkedListFromModel(self._model_model)
1313        poly_params = self.checkedListFromModel(self._poly_model)
1314        magnet_params = self.checkedListFromModel(self._magnet_model)
1315
1316        # Retrieve poly params names
1317        poly_params = [param.rsplit()[-1] + '.width' for param in poly_params]
1318
1319        self.parameters_to_fit = main_params + poly_params + magnet_params
1320
1321    def checkedListFromModel(self, model):
1322        """
1323        Returns list of checked parameters for given model
1324        """
1325        def isChecked(row):
1326            return model.item(row, 0).checkState() == QtCore.Qt.Checked
1327
1328        return [str(model.item(row_index, 0).text())
1329                for row_index in xrange(model.rowCount())
1330                if isChecked(row_index)]
1331
1332    def nameForFittedData(self, name):
1333        """
1334        Generate name for the current fit
1335        """
1336        if self.is2D:
1337            name += "2d"
1338        name = "M%i [%s]" % (self.tab_id, name)
1339        return name
1340
1341    def createNewIndex(self, fitted_data):
1342        """
1343        Create a model or theory index with passed Data1D/Data2D
1344        """
1345        if self.data_is_loaded:
1346            if not fitted_data.name:
1347                name = self.nameForFittedData(self.data.filename)
1348                fitted_data.title = name
1349                fitted_data.name = name
1350                fitted_data.filename = name
1351                fitted_data.symbol = "Line"
1352            self.updateModelIndex(fitted_data)
1353        else:
1354            name = self.nameForFittedData(self.kernel_module.name)
1355            fitted_data.title = name
1356            fitted_data.name = name
1357            fitted_data.filename = name
1358            fitted_data.symbol = "Line"
1359            self.createTheoryIndex(fitted_data)
1360
1361    def updateModelIndex(self, fitted_data):
1362        """
1363        Update a QStandardModelIndex containing model data
1364        """
1365        name = self.nameFromData(fitted_data)
1366        # Make this a line if no other defined
1367        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
1368            fitted_data.symbol = 'Line'
1369        # Notify the GUI manager so it can update the main model in DataExplorer
1370        GuiUtils.updateModelItemWithPlot(self._index, QtCore.QVariant(fitted_data), name)
1371
1372    def createTheoryIndex(self, fitted_data):
1373        """
1374        Create a QStandardModelIndex containing model data
1375        """
1376        name = self.nameFromData(fitted_data)
1377        # Notify the GUI manager so it can create the theory model in DataExplorer
1378        new_item = GuiUtils.createModelItemWithPlot(QtCore.QVariant(fitted_data), name=name)
1379        self.communicate.updateTheoryFromPerspectiveSignal.emit(new_item)
1380
1381    def nameFromData(self, fitted_data):
1382        """
1383        Return name for the dataset. Terribly impure function.
1384        """
1385        if fitted_data.name is None:
1386            name = self.nameForFittedData(self.logic.data.filename)
1387            fitted_data.title = name
1388            fitted_data.name = name
1389            fitted_data.filename = name
1390        else:
1391            name = fitted_data.name
1392        return name
1393
1394    def methodCalculateForData(self):
1395        '''return the method for data calculation'''
1396        return Calc1D if isinstance(self.data, Data1D) else Calc2D
1397
1398    def methodCompleteForData(self):
1399        '''return the method for result parsin on calc complete '''
1400        return self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1401
1402    def calculateQGridForModel(self):
1403        """
1404        Prepare the fitting data object, based on current ModelModel
1405        """
1406        if self.kernel_module is None:
1407            return
1408        # Awful API to a backend method.
1409        method = self.methodCalculateForData()(data=self.data,
1410                                               model=self.kernel_module,
1411                                               page_id=0,
1412                                               qmin=self.q_range_min,
1413                                               qmax=self.q_range_max,
1414                                               smearer=None,
1415                                               state=None,
1416                                               weight=None,
1417                                               fid=None,
1418                                               toggle_mode_on=False,
1419                                               completefn=None,
1420                                               update_chisqr=True,
1421                                               exception_handler=self.calcException,
1422                                               source=None)
1423
1424        calc_thread = threads.deferToThread(method.compute)
1425        calc_thread.addCallback(self.methodCompleteForData())
1426        calc_thread.addErrback(self.calculateDataFailed)
1427
1428    def calculateDataFailed(self, reason):
1429        """
1430        Thread returned error
1431        """
1432        print "Calculate Data failed with ", reason
1433
1434    def complete1D(self, return_data):
1435        """
1436        Plot the current 1D data
1437        """
1438        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
1439        self.calculateResiduals(fitted_data)
1440        self.model_data = fitted_data
1441
1442    def complete2D(self, return_data):
1443        """
1444        Plot the current 2D data
1445        """
1446        fitted_data = self.logic.new2DPlot(return_data)
1447        self.calculateResiduals(fitted_data)
1448        self.model_data = fitted_data
1449
1450    def calculateResiduals(self, fitted_data):
1451        """
1452        Calculate and print Chi2 and display chart of residuals
1453        """
1454        # Create a new index for holding data
1455        fitted_data.symbol = "Line"
1456
1457        # Modify fitted_data with weighting
1458        self.addWeightingToData(fitted_data)
1459
1460        self.createNewIndex(fitted_data)
1461        # Calculate difference between return_data and logic.data
1462        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
1463        # Update the control
1464        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
1465        self.lblChi2Value.setText(chi2_repr)
1466
1467        self.communicate.plotUpdateSignal.emit([fitted_data])
1468
1469        # Plot residuals if actual data
1470        if not self.data_is_loaded:
1471            return
1472
1473        residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
1474        residuals_plot.id = "Residual " + residuals_plot.id
1475        self.createNewIndex(residuals_plot)
1476        self.communicate.plotUpdateSignal.emit([residuals_plot])
1477
1478    def calcException(self, etype, value, tb):
1479        """
1480        Thread threw an exception.
1481        """
1482        # TODO: remimplement thread cancellation
1483        logging.error("".join(traceback.format_exception(etype, value, tb)))
1484
1485    def setTableProperties(self, table):
1486        """
1487        Setting table properties
1488        """
1489        # Table properties
1490        table.verticalHeader().setVisible(False)
1491        table.setAlternatingRowColors(True)
1492        table.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
1493        table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
1494        table.resizeColumnsToContents()
1495
1496        # Header
1497        header = table.horizontalHeader()
1498        header.setResizeMode(QtGui.QHeaderView.ResizeToContents)
1499
1500        header.ResizeMode(QtGui.QHeaderView.Interactive)
1501        # Resize column 0 and 7 to content
1502        header.setResizeMode(0, QtGui.QHeaderView.ResizeToContents)
1503        header.setResizeMode(7, QtGui.QHeaderView.ResizeToContents)
1504
1505    def setPolyModel(self):
1506        """
1507        Set polydispersity values
1508        """
1509        if not self.model_parameters:
1510            return
1511        self._poly_model.clear()
1512
1513        [self.setPolyModelParameters(i, param) for i, param in \
1514            enumerate(self.model_parameters.form_volume_parameters) if param.polydisperse]
1515        FittingUtilities.addPolyHeadersToModel(self._poly_model)
1516
1517    def setPolyModelParameters(self, i, param):
1518        """
1519        Standard of multishell poly parameter driver
1520        """
1521        param_name = param.name
1522        # see it the parameter is multishell
1523        if '[' in param.name:
1524            # Skip empty shells
1525            if self.current_shell_displayed == 0:
1526                return
1527            else:
1528                # Create as many entries as current shells
1529                for ishell in xrange(1, self.current_shell_displayed+1):
1530                    # Remove [n] and add the shell numeral
1531                    name = param_name[0:param_name.index('[')] + str(ishell)
1532                    self.addNameToPolyModel(i, name)
1533        else:
1534            # Just create a simple param entry
1535            self.addNameToPolyModel(i, param_name)
1536
1537    def addNameToPolyModel(self, i, param_name):
1538        """
1539        Creates a checked row in the poly model with param_name
1540        """
1541        # Polydisp. values from the sasmodel
1542        width = self.kernel_module.getParam(param_name + '.width')
1543        npts = self.kernel_module.getParam(param_name + '.npts')
1544        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
1545        _, min, max = self.kernel_module.details[param_name]
1546
1547        # Construct a row with polydisp. related variable.
1548        # This will get added to the polydisp. model
1549        # Note: last argument needs extra space padding for decent display of the control
1550        checked_list = ["Distribution of " + param_name, str(width),
1551                        str(min), str(max),
1552                        str(npts), str(nsigs), "gaussian      ",'']
1553        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
1554
1555        # All possible polydisp. functions as strings in combobox
1556        func = QtGui.QComboBox()
1557        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.iterkeys()])
1558        # Set the default index
1559        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
1560        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
1561        self.lstPoly.setIndexWidget(ind, func)
1562        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
1563
1564    def onPolyFilenameChange(self, row_index):
1565        """
1566        Respond to filename_updated signal from the delegate
1567        """
1568        # For the given row, invoke the "array" combo handler
1569        array_caption = 'array'
1570
1571        # Get the combo box reference
1572        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
1573        widget = self.lstPoly.indexWidget(ind)
1574
1575        # Update the combo box so it displays "array"
1576        widget.blockSignals(True)
1577        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
1578        widget.blockSignals(False)
1579
1580        # Invoke the file reader
1581        self.onPolyComboIndexChange(array_caption, row_index)
1582
1583    def onPolyComboIndexChange(self, combo_string, row_index):
1584        """
1585        Modify polydisp. defaults on function choice
1586        """
1587        # Get npts/nsigs for current selection
1588        param = self.model_parameters.form_volume_parameters[row_index]
1589        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
1590        combo_box = self.lstPoly.indexWidget(file_index)
1591
1592        def updateFunctionCaption(row):
1593            # Utility function for update of polydispersity function name in the main model
1594            param_name = str(self._model_model.item(row, 0).text())
1595            if param_name !=  param.name:
1596                return
1597            # Modify the param value
1598            self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
1599
1600        if combo_string == 'array':
1601            try:
1602                self.loadPolydispArray(row_index)
1603                # Update main model for display
1604                self.iterateOverModel(updateFunctionCaption)
1605                # disable the row
1606                lo = self.lstPoly.itemDelegate().poly_pd
1607                hi = self.lstPoly.itemDelegate().poly_function
1608                [self._poly_model.item(row_index, i).setEnabled(False) for i in xrange(lo, hi)]
1609                return
1610            except IOError:
1611                combo_box.setCurrentIndex(self.orig_poly_index)
1612                # Pass for cancel/bad read
1613                pass
1614
1615        # Enable the row in case it was disabled by Array
1616        self._poly_model.blockSignals(True)
1617        max_range = self.lstPoly.itemDelegate().poly_filename
1618        [self._poly_model.item(row_index, i).setEnabled(True) for i in xrange(7)]
1619        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
1620        self._poly_model.setData(file_index, QtCore.QVariant(""))
1621        self._poly_model.blockSignals(False)
1622
1623        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
1624        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
1625
1626        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
1627        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
1628
1629        self._poly_model.setData(npts_index, QtCore.QVariant(npts))
1630        self._poly_model.setData(nsigs_index, QtCore.QVariant(nsigs))
1631
1632        self.iterateOverModel(updateFunctionCaption)
1633        self.orig_poly_index = combo_box.currentIndex()
1634
1635    def loadPolydispArray(self, row_index):
1636        """
1637        Show the load file dialog and loads requested data into state
1638        """
1639        datafile = QtGui.QFileDialog.getOpenFileName(
1640            self, "Choose a weight file", "", "All files (*.*)",
1641            None, QtGui.QFileDialog.DontUseNativeDialog)
1642
1643        if datafile is None or str(datafile)=='':
1644            logging.info("No weight data chosen.")
1645            raise IOError
1646
1647        values = []
1648        weights = []
1649        def appendData(data_tuple):
1650            """
1651            Fish out floats from a tuple of strings
1652            """
1653            try:
1654                values.append(float(data_tuple[0]))
1655                weights.append(float(data_tuple[1]))
1656            except (ValueError, IndexError):
1657                # just pass through if line with bad data
1658                return
1659
1660        with open(datafile, 'r') as column_file:
1661            column_data = [line.rstrip().split() for line in column_file.readlines()]
1662            [appendData(line) for line in column_data]
1663
1664        # If everything went well - update the sasmodel values
1665        self.disp_model = POLYDISPERSITY_MODELS['array']()
1666        self.disp_model.set_weights(np.array(values), np.array(weights))
1667        # + update the cell with filename
1668        fname = os.path.basename(str(datafile))
1669        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
1670        self._poly_model.setData(fname_index, QtCore.QVariant(fname))
1671
1672    def setMagneticModel(self):
1673        """
1674        Set magnetism values on model
1675        """
1676        if not self.model_parameters:
1677            return
1678        self._magnet_model.clear()
1679        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
1680            self.model_parameters.call_parameters if param.type == 'magnetic']
1681        FittingUtilities.addHeadersToModel(self._magnet_model)
1682
1683    def shellNamesList(self):
1684        """
1685        Returns list of names of all multi-shell parameters
1686        E.g. for sld[n], radius[n], n=1..3 it will return
1687        [sld1, sld2, sld3, radius1, radius2, radius3]
1688        """
1689        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
1690        top_index = self.kernel_module.multiplicity_info.number
1691        shell_names = []
1692        for i in xrange(1, top_index+1):
1693            for name in multi_names:
1694                shell_names.append(name+str(i))
1695        return shell_names
1696
1697    def addCheckedMagneticListToModel(self, param, model):
1698        """
1699        Wrapper for model update with a subset of magnetic parameters
1700        """
1701        if param.name[param.name.index(':')+1:] in self.shell_names:
1702            # check if two-digit shell number
1703            try:
1704                shell_index = int(param.name[-2:])
1705            except ValueError:
1706                shell_index = int(param.name[-1:])
1707
1708            if shell_index > self.current_shell_displayed:
1709                return
1710
1711        checked_list = [param.name,
1712                        str(param.default),
1713                        str(param.limits[0]),
1714                        str(param.limits[1]),
1715                        param.units]
1716
1717        FittingUtilities.addCheckedListToModel(model, checked_list)
1718
1719    def enableStructureFactorControl(self, structure_factor):
1720        """
1721        Add structure factors to the list of parameters
1722        """
1723        if self.kernel_module.is_form_factor or structure_factor == 'None':
1724            self.enableStructureCombo()
1725        else:
1726            self.disableStructureCombo()
1727
1728    def addExtraShells(self):
1729        """
1730        Add a combobox for multiple shell display
1731        """
1732        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
1733
1734        if param_length == 0:
1735            return
1736
1737        # cell 1: variable name
1738        item1 = QtGui.QStandardItem(param_name)
1739
1740        func = QtGui.QComboBox()
1741        # Available range of shells displayed in the combobox
1742        func.addItems([str(i) for i in xrange(param_length+1)])
1743
1744        # Respond to index change
1745        func.currentIndexChanged.connect(self.modifyShellsInList)
1746
1747        # cell 2: combobox
1748        item2 = QtGui.QStandardItem()
1749        self._model_model.appendRow([item1, item2])
1750
1751        # Beautify the row:  span columns 2-4
1752        shell_row = self._model_model.rowCount()
1753        shell_index = self._model_model.index(shell_row-1, 1)
1754
1755        self.lstParams.setIndexWidget(shell_index, func)
1756        self._last_model_row = self._model_model.rowCount()
1757
1758        # Set the index to the state-kept value
1759        func.setCurrentIndex(self.current_shell_displayed
1760                             if self.current_shell_displayed < func.count() else 0)
1761
1762    def modifyShellsInList(self, index):
1763        """
1764        Add/remove additional multishell parameters
1765        """
1766        # Find row location of the combobox
1767        last_row = self._last_model_row
1768        remove_rows = self._model_model.rowCount() - last_row
1769
1770        if remove_rows > 1:
1771            self._model_model.removeRows(last_row, remove_rows)
1772
1773        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
1774        self.current_shell_displayed = index
1775
1776        # Update relevant models
1777        self.setPolyModel()
1778        self.setMagneticModel()
1779
1780    def readFitPage(self, fp):
1781        """
1782        Read in state from a fitpage object and update GUI
1783        """
1784        assert isinstance(fp, FitPage)
1785        # Main tab info
1786        self.logic.data.filename = fp.filename
1787        self.data_is_loaded = fp.data_is_loaded
1788        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
1789        self.chkMagnetism.setCheckState(fp.is_magnetic)
1790        self.chk2DView.setCheckState(fp.is2D)
1791
1792        # Update the comboboxes
1793        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
1794        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
1795        if fp.current_factor:
1796            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
1797
1798        self.chi2 = fp.chi2
1799
1800        # Options tab
1801        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
1802        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
1803        self.npts = fp.fit_options[fp.NPTS]
1804        self.log_points = fp.fit_options[fp.LOG_POINTS]
1805        self.weighting = fp.fit_options[fp.WEIGHTING]
1806
1807        # Models
1808        self._model_model = fp.model_model
1809        self._poly_model = fp.poly_model
1810        self._magnet_model = fp.magnetism_model
1811
1812        # Resolution tab
1813        smearing = fp.smearing_options[fp.SMEARING_OPTION]
1814        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
1815        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
1816        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
1817        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
1818
1819        # TODO: add polidyspersity and magnetism
1820
1821    def saveToFitPage(self, fp):
1822        """
1823        Write current state to the given fitpage
1824        """
1825        assert isinstance(fp, FitPage)
1826
1827        # Main tab info
1828        fp.filename = self.logic.data.filename
1829        fp.data_is_loaded = self.data_is_loaded
1830        fp.is_polydisperse = self.chkPolydispersity.isChecked()
1831        fp.is_magnetic = self.chkMagnetism.isChecked()
1832        fp.is2D = self.chk2DView.isChecked()
1833        fp.data = self.data
1834
1835        # Use current models - they contain all the required parameters
1836        fp.model_model = self._model_model
1837        fp.poly_model = self._poly_model
1838        fp.magnetism_model = self._magnet_model
1839
1840        if self.cbCategory.currentIndex() != 0:
1841            fp.current_category = str(self.cbCategory.currentText())
1842            fp.current_model = str(self.cbModel.currentText())
1843
1844        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
1845            fp.current_factor = str(self.cbStructureFactor.currentText())
1846        else:
1847            fp.current_factor = ''
1848
1849        fp.chi2 = self.chi2
1850        fp.parameters_to_fit = self.parameters_to_fit
1851        fp.kernel_module = self.kernel_module
1852
1853        # Algorithm options
1854        # fp.algorithm = self.parent.fit_options.selected_id
1855
1856        # Options tab
1857        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
1858        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
1859        fp.fit_options[fp.NPTS] = self.npts
1860        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
1861        fp.fit_options[fp.LOG_POINTS] = self.log_points
1862        fp.fit_options[fp.WEIGHTING] = self.weighting
1863
1864        # Resolution tab
1865        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
1866        fp.smearing_options[fp.SMEARING_OPTION] = smearing
1867        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
1868        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
1869        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
1870
1871        # TODO: add polidyspersity and magnetism
1872
1873
1874    def updateUndo(self):
1875        """
1876        Create a new state page and add it to the stack
1877        """
1878        if self.undo_supported:
1879            self.pushFitPage(self.currentState())
1880
1881    def currentState(self):
1882        """
1883        Return fit page with current state
1884        """
1885        new_page = FitPage()
1886        self.saveToFitPage(new_page)
1887
1888        return new_page
1889
1890    def pushFitPage(self, new_page):
1891        """
1892        Add a new fit page object with current state
1893        """
1894        self.page_stack.append(new_page)
1895
1896    def popFitPage(self):
1897        """
1898        Remove top fit page from stack
1899        """
1900        if self.page_stack:
1901            self.page_stack.pop()
1902
Note: See TracBrowser for help on using the repository browser.