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

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

Fix fitting table behaviour for when the error column is added

  • Property mode set to 100644
File size: 70.8 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.lstParams.itemDelegate().addErrorColumn()
874        self.iterateOverModel(createErrorColumn)
875
876        # switch off reponse to model change
877        self._model_model.blockSignals(True)
878        self._model_model.insertColumn(2, error_column)
879        self._model_model.blockSignals(False)
880        FittingUtilities.addErrorHeadersToModel(self._model_model)
881        # Adjust the table cells width.
882        # TODO: find a way to dynamically adjust column width while resized expanding
883        self.lstParams.resizeColumnToContents(0)
884        self.lstParams.resizeColumnToContents(4)
885        self.lstParams.resizeColumnToContents(5)
886        self.lstParams.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
887
888        self.has_error_column = True
889
890    def updatePolyModelFromList(self, param_dict):
891        """
892        Update the polydispersity model with new parameters, create the errors column
893        """
894        assert isinstance(param_dict, dict)
895        if not dict:
896            return
897
898        def iterateOverPolyModel(func):
899            """
900            Take func and throw it inside the poly model row loop
901            """
902            for row_i in xrange(self._poly_model.rowCount()):
903                func(row_i)
904
905        def updateFittedValues(row_i):
906            # Utility function for main model update
907            # internal so can use closure for param_dict
908            if row_i >= self._poly_model.rowCount():
909                return
910            param_name = str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
911            if param_name not in param_dict.keys():
912                return
913            # modify the param value
914            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
915            self._poly_model.item(row_i, 1).setText(param_repr)
916            if self.has_poly_error_column:
917                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
918                self._poly_model.item(row_i, 2).setText(error_repr)
919
920
921        def createErrorColumn(row_i):
922            # Utility function for error column update
923            if row_i >= self._poly_model.rowCount():
924                return
925            item = QtGui.QStandardItem()
926
927            def createItem(param_name):
928                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
929                item.setText(error_repr)
930
931            def poly_param():
932                return str(self._poly_model.item(row_i, 0).text()).rsplit()[-1] + '.width'
933
934            [createItem(param_name) for param_name in param_dict.keys() if poly_param() == param_name]
935
936            error_column.append(item)
937
938        # block signals temporarily, so we don't end up
939        # updating charts with every single model change on the end of fitting
940        self._poly_model.blockSignals(True)
941        iterateOverPolyModel(updateFittedValues)
942        self._poly_model.blockSignals(False)
943
944        if self.has_poly_error_column:
945            return
946
947        self.lstPoly.itemDelegate().addErrorColumn()
948        error_column = []
949        iterateOverPolyModel(createErrorColumn)
950
951        # switch off reponse to model change
952        self._poly_model.blockSignals(True)
953        self._poly_model.insertColumn(2, error_column)
954        self._poly_model.blockSignals(False)
955        FittingUtilities.addErrorPolyHeadersToModel(self._poly_model)
956
957        self.has_poly_error_column = True
958
959    def updateMagnetModelFromList(self, param_dict):
960        """
961        Update the magnetic model with new parameters, create the errors column
962        """
963        assert isinstance(param_dict, dict)
964        if not dict:
965            return
966
967        def iterateOverMagnetModel(func):
968            """
969            Take func and throw it inside the magnet model row loop
970            """
971            for row_i in xrange(self._model_model.rowCount()):
972                func(row_i)
973
974        def updateFittedValues(row):
975            # Utility function for main model update
976            # internal so can use closure for param_dict
977            param_name = str(self._magnet_model.item(row, 0).text())
978            if param_name not in param_dict.keys():
979                return
980            # modify the param value
981            param_repr = GuiUtils.formatNumber(param_dict[param_name][0], high=True)
982            self._magnet_model.item(row, 1).setText(param_repr)
983            if self.has_magnet_error_column:
984                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
985                self._magnet_model.item(row, 2).setText(error_repr)
986
987        def createErrorColumn(row):
988            # Utility function for error column update
989            item = QtGui.QStandardItem()
990            def createItem(param_name):
991                error_repr = GuiUtils.formatNumber(param_dict[param_name][1], high=True)
992                item.setText(error_repr)
993            def curr_param():
994                return str(self._magnet_model.item(row, 0).text())
995
996            [createItem(param_name) for param_name in param_dict.keys() if curr_param() == param_name]
997
998            error_column.append(item)
999
1000        # block signals temporarily, so we don't end up
1001        # updating charts with every single model change on the end of fitting
1002        self._magnet_model.blockSignals(True)
1003        iterateOverMagnetModel(updateFittedValues)
1004        self._magnet_model.blockSignals(False)
1005
1006        if self.has_magnet_error_column:
1007            return
1008
1009        self.lstMagnetic.itemDelegate().addErrorColumn()
1010        error_column = []
1011        iterateOverMagnetModel(createErrorColumn)
1012
1013        # switch off reponse to model change
1014        self._magnet_model.blockSignals(True)
1015        self._magnet_model.insertColumn(2, error_column)
1016        self._magnet_model.blockSignals(False)
1017        FittingUtilities.addErrorHeadersToModel(self._magnet_model)
1018
1019        self.has_magnet_error_column = True
1020
1021    def onPlot(self):
1022        """
1023        Plot the current set of data
1024        """
1025        # Regardless of previous state, this should now be `plot show` functionality only
1026        self.cmdPlot.setText("Show Plot")
1027        if not self.data_is_loaded:
1028            self.recalculatePlotData()
1029        self.showPlot()
1030
1031    def recalculatePlotData(self):
1032        """
1033        Generate a new dataset for model
1034        """
1035        if not self.data_is_loaded:
1036            self.createDefaultDataset()
1037        self.calculateQGridForModel()
1038
1039    def showPlot(self):
1040        """
1041        Show the current plot in MPL
1042        """
1043        # Show the chart if ready
1044        data_to_show = self.data if self.data_is_loaded else self.model_data
1045        if data_to_show is not None:
1046            self.communicate.plotRequestedSignal.emit([data_to_show])
1047
1048    def onOptionsUpdate(self):
1049        """
1050        Update local option values and replot
1051        """
1052        self.q_range_min, self.q_range_max, self.npts, self.log_points, self.weighting = \
1053            self.options_widget.state()
1054        # set Q range labels on the main tab
1055        self.lblMinRangeDef.setText(str(self.q_range_min))
1056        self.lblMaxRangeDef.setText(str(self.q_range_max))
1057        self.recalculatePlotData()
1058
1059    def setDefaultStructureCombo(self):
1060        """
1061        Fill in the structure factors combo box with defaults
1062        """
1063        structure_factor_list = self.master_category_dict.pop(CATEGORY_STRUCTURE)
1064        factors = [factor[0] for factor in structure_factor_list]
1065        factors.insert(0, STRUCTURE_DEFAULT)
1066        self.cbStructureFactor.clear()
1067        self.cbStructureFactor.addItems(sorted(factors))
1068
1069    def createDefaultDataset(self):
1070        """
1071        Generate default Dataset 1D/2D for the given model
1072        """
1073        # Create default datasets if no data passed
1074        if self.is2D:
1075            qmax = self.q_range_max/np.sqrt(2)
1076            qstep = self.npts
1077            self.logic.createDefault2dData(qmax, qstep, self.tab_id)
1078            return
1079        elif self.log_points:
1080            qmin = -10.0 if self.q_range_min < 1.e-10 else np.log10(self.q_range_min)
1081            qmax = 10.0 if self.q_range_max > 1.e10 else np.log10(self.q_range_max)
1082            interval = np.logspace(start=qmin, stop=qmax, num=self.npts, endpoint=True, base=10.0)
1083        else:
1084            interval = np.linspace(start=self.q_range_min, stop=self.q_range_max,
1085                                   num=self.npts, endpoint=True)
1086        self.logic.createDefault1dData(interval, self.tab_id)
1087
1088    def readCategoryInfo(self):
1089        """
1090        Reads the categories in from file
1091        """
1092        self.master_category_dict = defaultdict(list)
1093        self.by_model_dict = defaultdict(list)
1094        self.model_enabled_dict = defaultdict(bool)
1095
1096        categorization_file = CategoryInstaller.get_user_file()
1097        if not os.path.isfile(categorization_file):
1098            categorization_file = CategoryInstaller.get_default_file()
1099        with open(categorization_file, 'rb') as cat_file:
1100            self.master_category_dict = json.load(cat_file)
1101            self.regenerateModelDict()
1102
1103        # Load the model dict
1104        models = load_standard_models()
1105        for model in models:
1106            self.models[model.name] = model
1107
1108    def regenerateModelDict(self):
1109        """
1110        Regenerates self.by_model_dict which has each model name as the
1111        key and the list of categories belonging to that model
1112        along with the enabled mapping
1113        """
1114        self.by_model_dict = defaultdict(list)
1115        for category in self.master_category_dict:
1116            for (model, enabled) in self.master_category_dict[category]:
1117                self.by_model_dict[model].append(category)
1118                self.model_enabled_dict[model] = enabled
1119
1120    def addBackgroundToModel(self, model):
1121        """
1122        Adds background parameter with default values to the model
1123        """
1124        assert isinstance(model, QtGui.QStandardItemModel)
1125        checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
1126        FittingUtilities.addCheckedListToModel(model, checked_list)
1127        last_row = model.rowCount()-1
1128        model.item(last_row, 0).setEditable(False)
1129        model.item(last_row, 4).setEditable(False)
1130
1131    def addScaleToModel(self, model):
1132        """
1133        Adds scale parameter with default values to the model
1134        """
1135        assert isinstance(model, QtGui.QStandardItemModel)
1136        checked_list = ['scale', '1.0', '0.0', 'inf', '']
1137        FittingUtilities.addCheckedListToModel(model, checked_list)
1138        last_row = model.rowCount()-1
1139        model.item(last_row, 0).setEditable(False)
1140        model.item(last_row, 4).setEditable(False)
1141
1142    def addWeightingToData(self, data):
1143        """
1144        Adds weighting contribution to fitting data
1145        """
1146        # Send original data for weighting
1147        weight = FittingUtilities.getWeight(data=data, is2d=self.is2D, flag=self.weighting)
1148        update_module = data.err_data if self.is2D else data.dy
1149        # Overwrite relevant values in data
1150        update_module = weight
1151
1152    def updateQRange(self):
1153        """
1154        Updates Q Range display
1155        """
1156        if self.data_is_loaded:
1157            self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange()
1158        # set Q range labels on the main tab
1159        self.lblMinRangeDef.setText(str(self.q_range_min))
1160        self.lblMaxRangeDef.setText(str(self.q_range_max))
1161        # set Q range labels on the options tab
1162        self.options_widget.updateQRange(self.q_range_min, self.q_range_max, self.npts)
1163
1164    def SASModelToQModel(self, model_name, structure_factor=None):
1165        """
1166        Setting model parameters into table based on selected category
1167        """
1168        # Crete/overwrite model items
1169        self._model_model.clear()
1170
1171        # First, add parameters from the main model
1172        if model_name is not None:
1173            self.fromModelToQModel(model_name)
1174
1175        # Then, add structure factor derived parameters
1176        if structure_factor is not None and structure_factor != "None":
1177            if model_name is None:
1178                # Instantiate the current sasmodel for SF-only models
1179                self.kernel_module = self.models[structure_factor]()
1180            self.fromStructureFactorToQModel(structure_factor)
1181        else:
1182            # Allow the SF combobox visibility for the given sasmodel
1183            self.enableStructureFactorControl(structure_factor)
1184
1185        # Then, add multishells
1186        if model_name is not None:
1187            # Multishell models need additional treatment
1188            self.addExtraShells()
1189
1190        # Add polydispersity to the model
1191        self.setPolyModel()
1192        # Add magnetic parameters to the model
1193        self.setMagneticModel()
1194
1195        # Adjust the table cells width
1196        self.lstParams.resizeColumnToContents(0)
1197        self.lstParams.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
1198
1199        # Now we claim the model has been loaded
1200        self.model_is_loaded = True
1201
1202        # (Re)-create headers
1203        FittingUtilities.addHeadersToModel(self._model_model)
1204        self.lstParams.header().setFont(self.boldFont)
1205
1206        # Update Q Ranges
1207        self.updateQRange()
1208
1209    def fromModelToQModel(self, model_name):
1210        """
1211        Setting model parameters into QStandardItemModel based on selected _model_
1212        """
1213        kernel_module = generate.load_kernel_module(model_name)
1214        self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', []))
1215
1216        # Instantiate the current sasmodel
1217        self.kernel_module = self.models[model_name]()
1218
1219        # Explicitly add scale and background with default values
1220        temp_undo_state = self.undo_supported
1221        self.undo_supported = False
1222        self.addScaleToModel(self._model_model)
1223        self.addBackgroundToModel(self._model_model)
1224        self.undo_supported = temp_undo_state
1225
1226        self.shell_names = self.shellNamesList()
1227
1228        # Update the QModel
1229        new_rows = FittingUtilities.addParametersToModel(self.model_parameters, self.kernel_module, self.is2D)
1230
1231        for row in new_rows:
1232            self._model_model.appendRow(row)
1233        # Update the counter used for multishell display
1234        self._last_model_row = self._model_model.rowCount()
1235
1236    def fromStructureFactorToQModel(self, structure_factor):
1237        """
1238        Setting model parameters into QStandardItemModel based on selected _structure factor_
1239        """
1240        structure_module = generate.load_kernel_module(structure_factor)
1241        structure_parameters = modelinfo.make_parameter_table(getattr(structure_module, 'parameters', []))
1242
1243        new_rows = FittingUtilities.addSimpleParametersToModel(structure_parameters, self.is2D)
1244        for row in new_rows:
1245            self._model_model.appendRow(row)
1246        # Update the counter used for multishell display
1247        self._last_model_row = self._model_model.rowCount()
1248
1249    def onMainParamsChange(self, item):
1250        """
1251        Callback method for updating the sasmodel parameters with the GUI values
1252        """
1253        model_column = item.column()
1254
1255        if model_column == 0:
1256            self.checkboxSelected(item)
1257            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
1258            # Update state stack
1259            self.updateUndo()
1260            return
1261
1262        model_row = item.row()
1263        name_index = self._model_model.index(model_row, 0)
1264
1265        # Extract changed value.
1266        try:
1267            value = float(item.text())
1268        except ValueError:
1269            # Unparsable field
1270            return
1271        parameter_name = str(self._model_model.data(name_index).toPyObject()) # sld, background 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        param_column = self.lstParams.itemDelegate().param_value
1278        min_column = self.lstParams.itemDelegate().param_min
1279        max_column = self.lstParams.itemDelegate().param_max
1280        if model_column == param_column:
1281            self.kernel_module.setParam(parameter_name, value)
1282        elif model_column == min_column:
1283            # min/max to be changed in self.kernel_module.details[parameter_name] = ['Ang', 0.0, inf]
1284            self.kernel_module.details[parameter_name][1] = value
1285        elif model_column == max_column:
1286            self.kernel_module.details[parameter_name][2] = value
1287        else:
1288            # don't update the chart
1289            return
1290
1291        # TODO: magnetic params in self.kernel_module.details['M0:parameter_name'] = value
1292        # TODO: multishell params in self.kernel_module.details[??] = value
1293
1294        # Force the chart update when actual parameters changed
1295        if model_column == 1:
1296            self.recalculatePlotData()
1297
1298        # Update state stack
1299        self.updateUndo()
1300
1301    def checkboxSelected(self, item):
1302        # Assure we're dealing with checkboxes
1303        if not item.isCheckable():
1304            return
1305        status = item.checkState()
1306
1307        def isCheckable(row):
1308            return self._model_model.item(row, 0).isCheckable()
1309
1310        # If multiple rows selected - toggle all of them, filtering uncheckable
1311        rows = [s.row() for s in self.lstParams.selectionModel().selectedRows() if isCheckable(s.row())]
1312
1313        # Switch off signaling from the model to avoid recursion
1314        self._model_model.blockSignals(True)
1315        # Convert to proper indices and set requested enablement
1316        [self._model_model.item(row, 0).setCheckState(status) for row in rows]
1317        self._model_model.blockSignals(False)
1318
1319        # update the list of parameters to fit
1320        main_params = self.checkedListFromModel(self._model_model)
1321        poly_params = self.checkedListFromModel(self._poly_model)
1322        magnet_params = self.checkedListFromModel(self._magnet_model)
1323
1324        # Retrieve poly params names
1325        poly_params = [param.rsplit()[-1] + '.width' for param in poly_params]
1326
1327        self.parameters_to_fit = main_params + poly_params + magnet_params
1328
1329    def checkedListFromModel(self, model):
1330        """
1331        Returns list of checked parameters for given model
1332        """
1333        def isChecked(row):
1334            return model.item(row, 0).checkState() == QtCore.Qt.Checked
1335
1336        return [str(model.item(row_index, 0).text())
1337                for row_index in xrange(model.rowCount())
1338                if isChecked(row_index)]
1339
1340    def nameForFittedData(self, name):
1341        """
1342        Generate name for the current fit
1343        """
1344        if self.is2D:
1345            name += "2d"
1346        name = "M%i [%s]" % (self.tab_id, name)
1347        return name
1348
1349    def createNewIndex(self, fitted_data):
1350        """
1351        Create a model or theory index with passed Data1D/Data2D
1352        """
1353        if self.data_is_loaded:
1354            if not fitted_data.name:
1355                name = self.nameForFittedData(self.data.filename)
1356                fitted_data.title = name
1357                fitted_data.name = name
1358                fitted_data.filename = name
1359                fitted_data.symbol = "Line"
1360            self.updateModelIndex(fitted_data)
1361        else:
1362            name = self.nameForFittedData(self.kernel_module.name)
1363            fitted_data.title = name
1364            fitted_data.name = name
1365            fitted_data.filename = name
1366            fitted_data.symbol = "Line"
1367            self.createTheoryIndex(fitted_data)
1368
1369    def updateModelIndex(self, fitted_data):
1370        """
1371        Update a QStandardModelIndex containing model data
1372        """
1373        name = self.nameFromData(fitted_data)
1374        # Make this a line if no other defined
1375        if hasattr(fitted_data, 'symbol') and fitted_data.symbol is None:
1376            fitted_data.symbol = 'Line'
1377        # Notify the GUI manager so it can update the main model in DataExplorer
1378        GuiUtils.updateModelItemWithPlot(self._index, QtCore.QVariant(fitted_data), name)
1379
1380    def createTheoryIndex(self, fitted_data):
1381        """
1382        Create a QStandardModelIndex containing model data
1383        """
1384        name = self.nameFromData(fitted_data)
1385        # Notify the GUI manager so it can create the theory model in DataExplorer
1386        new_item = GuiUtils.createModelItemWithPlot(QtCore.QVariant(fitted_data), name=name)
1387        self.communicate.updateTheoryFromPerspectiveSignal.emit(new_item)
1388
1389    def nameFromData(self, fitted_data):
1390        """
1391        Return name for the dataset. Terribly impure function.
1392        """
1393        if fitted_data.name is None:
1394            name = self.nameForFittedData(self.logic.data.filename)
1395            fitted_data.title = name
1396            fitted_data.name = name
1397            fitted_data.filename = name
1398        else:
1399            name = fitted_data.name
1400        return name
1401
1402    def methodCalculateForData(self):
1403        '''return the method for data calculation'''
1404        return Calc1D if isinstance(self.data, Data1D) else Calc2D
1405
1406    def methodCompleteForData(self):
1407        '''return the method for result parsin on calc complete '''
1408        return self.complete1D if isinstance(self.data, Data1D) else self.complete2D
1409
1410    def calculateQGridForModel(self):
1411        """
1412        Prepare the fitting data object, based on current ModelModel
1413        """
1414        if self.kernel_module is None:
1415            return
1416        # Awful API to a backend method.
1417        method = self.methodCalculateForData()(data=self.data,
1418                                               model=self.kernel_module,
1419                                               page_id=0,
1420                                               qmin=self.q_range_min,
1421                                               qmax=self.q_range_max,
1422                                               smearer=None,
1423                                               state=None,
1424                                               weight=None,
1425                                               fid=None,
1426                                               toggle_mode_on=False,
1427                                               completefn=None,
1428                                               update_chisqr=True,
1429                                               exception_handler=self.calcException,
1430                                               source=None)
1431
1432        calc_thread = threads.deferToThread(method.compute)
1433        calc_thread.addCallback(self.methodCompleteForData())
1434        calc_thread.addErrback(self.calculateDataFailed)
1435
1436    def calculateDataFailed(self, reason):
1437        """
1438        Thread returned error
1439        """
1440        print "Calculate Data failed with ", reason
1441
1442    def complete1D(self, return_data):
1443        """
1444        Plot the current 1D data
1445        """
1446        fitted_data = self.logic.new1DPlot(return_data, self.tab_id)
1447        self.calculateResiduals(fitted_data)
1448        self.model_data = fitted_data
1449
1450    def complete2D(self, return_data):
1451        """
1452        Plot the current 2D data
1453        """
1454        fitted_data = self.logic.new2DPlot(return_data)
1455        self.calculateResiduals(fitted_data)
1456        self.model_data = fitted_data
1457
1458    def calculateResiduals(self, fitted_data):
1459        """
1460        Calculate and print Chi2 and display chart of residuals
1461        """
1462        # Create a new index for holding data
1463        fitted_data.symbol = "Line"
1464
1465        # Modify fitted_data with weighting
1466        self.addWeightingToData(fitted_data)
1467
1468        self.createNewIndex(fitted_data)
1469        # Calculate difference between return_data and logic.data
1470        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
1471        # Update the control
1472        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
1473        self.lblChi2Value.setText(chi2_repr)
1474
1475        self.communicate.plotUpdateSignal.emit([fitted_data])
1476
1477        # Plot residuals if actual data
1478        if not self.data_is_loaded:
1479            return
1480
1481        residuals_plot = FittingUtilities.plotResiduals(self.data, fitted_data)
1482        residuals_plot.id = "Residual " + residuals_plot.id
1483        self.createNewIndex(residuals_plot)
1484        self.communicate.plotUpdateSignal.emit([residuals_plot])
1485
1486    def calcException(self, etype, value, tb):
1487        """
1488        Thread threw an exception.
1489        """
1490        # TODO: remimplement thread cancellation
1491        logging.error("".join(traceback.format_exception(etype, value, tb)))
1492
1493    def setTableProperties(self, table):
1494        """
1495        Setting table properties
1496        """
1497        # Table properties
1498        table.verticalHeader().setVisible(False)
1499        table.setAlternatingRowColors(True)
1500        table.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Expanding)
1501        table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
1502        table.resizeColumnsToContents()
1503
1504        # Header
1505        header = table.horizontalHeader()
1506        header.setResizeMode(QtGui.QHeaderView.ResizeToContents)
1507
1508        header.ResizeMode(QtGui.QHeaderView.Interactive)
1509        # Resize column 0 and 7 to content
1510        header.setResizeMode(0, QtGui.QHeaderView.ResizeToContents)
1511        header.setResizeMode(7, QtGui.QHeaderView.ResizeToContents)
1512
1513    def setPolyModel(self):
1514        """
1515        Set polydispersity values
1516        """
1517        if not self.model_parameters:
1518            return
1519        self._poly_model.clear()
1520
1521        [self.setPolyModelParameters(i, param) for i, param in \
1522            enumerate(self.model_parameters.form_volume_parameters) if param.polydisperse]
1523        FittingUtilities.addPolyHeadersToModel(self._poly_model)
1524
1525    def setPolyModelParameters(self, i, param):
1526        """
1527        Standard of multishell poly parameter driver
1528        """
1529        param_name = param.name
1530        # see it the parameter is multishell
1531        if '[' in param.name:
1532            # Skip empty shells
1533            if self.current_shell_displayed == 0:
1534                return
1535            else:
1536                # Create as many entries as current shells
1537                for ishell in xrange(1, self.current_shell_displayed+1):
1538                    # Remove [n] and add the shell numeral
1539                    name = param_name[0:param_name.index('[')] + str(ishell)
1540                    self.addNameToPolyModel(i, name)
1541        else:
1542            # Just create a simple param entry
1543            self.addNameToPolyModel(i, param_name)
1544
1545    def addNameToPolyModel(self, i, param_name):
1546        """
1547        Creates a checked row in the poly model with param_name
1548        """
1549        # Polydisp. values from the sasmodel
1550        width = self.kernel_module.getParam(param_name + '.width')
1551        npts = self.kernel_module.getParam(param_name + '.npts')
1552        nsigs = self.kernel_module.getParam(param_name + '.nsigmas')
1553        _, min, max = self.kernel_module.details[param_name]
1554
1555        # Construct a row with polydisp. related variable.
1556        # This will get added to the polydisp. model
1557        # Note: last argument needs extra space padding for decent display of the control
1558        checked_list = ["Distribution of " + param_name, str(width),
1559                        str(min), str(max),
1560                        str(npts), str(nsigs), "gaussian      ",'']
1561        FittingUtilities.addCheckedListToModel(self._poly_model, checked_list)
1562
1563        # All possible polydisp. functions as strings in combobox
1564        func = QtGui.QComboBox()
1565        func.addItems([str(name_disp) for name_disp in POLYDISPERSITY_MODELS.iterkeys()])
1566        # Set the default index
1567        func.setCurrentIndex(func.findText(DEFAULT_POLYDISP_FUNCTION))
1568        ind = self._poly_model.index(i,self.lstPoly.itemDelegate().poly_function)
1569        self.lstPoly.setIndexWidget(ind, func)
1570        func.currentIndexChanged.connect(lambda: self.onPolyComboIndexChange(str(func.currentText()), i))
1571
1572    def onPolyFilenameChange(self, row_index):
1573        """
1574        Respond to filename_updated signal from the delegate
1575        """
1576        # For the given row, invoke the "array" combo handler
1577        array_caption = 'array'
1578
1579        # Get the combo box reference
1580        ind = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
1581        widget = self.lstPoly.indexWidget(ind)
1582
1583        # Update the combo box so it displays "array"
1584        widget.blockSignals(True)
1585        widget.setCurrentIndex(self.lstPoly.itemDelegate().POLYDISPERSE_FUNCTIONS.index(array_caption))
1586        widget.blockSignals(False)
1587
1588        # Invoke the file reader
1589        self.onPolyComboIndexChange(array_caption, row_index)
1590
1591    def onPolyComboIndexChange(self, combo_string, row_index):
1592        """
1593        Modify polydisp. defaults on function choice
1594        """
1595        # Get npts/nsigs for current selection
1596        param = self.model_parameters.form_volume_parameters[row_index]
1597        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_function)
1598        combo_box = self.lstPoly.indexWidget(file_index)
1599
1600        def updateFunctionCaption(row):
1601            # Utility function for update of polydispersity function name in the main model
1602            param_name = str(self._model_model.item(row, 0).text())
1603            if param_name !=  param.name:
1604                return
1605            # Modify the param value
1606            self._model_model.item(row, 0).child(0).child(0,4).setText(combo_string)
1607
1608        if combo_string == 'array':
1609            try:
1610                self.loadPolydispArray(row_index)
1611                # Update main model for display
1612                self.iterateOverModel(updateFunctionCaption)
1613                # disable the row
1614                lo = self.lstPoly.itemDelegate().poly_pd
1615                hi = self.lstPoly.itemDelegate().poly_function
1616                [self._poly_model.item(row_index, i).setEnabled(False) for i in xrange(lo, hi)]
1617                return
1618            except IOError:
1619                combo_box.setCurrentIndex(self.orig_poly_index)
1620                # Pass for cancel/bad read
1621                pass
1622
1623        # Enable the row in case it was disabled by Array
1624        self._poly_model.blockSignals(True)
1625        max_range = self.lstPoly.itemDelegate().poly_filename
1626        [self._poly_model.item(row_index, i).setEnabled(True) for i in xrange(7)]
1627        file_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
1628        self._poly_model.setData(file_index, QtCore.QVariant(""))
1629        self._poly_model.blockSignals(False)
1630
1631        npts_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_npts)
1632        nsigs_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_nsigs)
1633
1634        npts = POLYDISPERSITY_MODELS[str(combo_string)].default['npts']
1635        nsigs = POLYDISPERSITY_MODELS[str(combo_string)].default['nsigmas']
1636
1637        self._poly_model.setData(npts_index, QtCore.QVariant(npts))
1638        self._poly_model.setData(nsigs_index, QtCore.QVariant(nsigs))
1639
1640        self.iterateOverModel(updateFunctionCaption)
1641        self.orig_poly_index = combo_box.currentIndex()
1642
1643    def loadPolydispArray(self, row_index):
1644        """
1645        Show the load file dialog and loads requested data into state
1646        """
1647        datafile = QtGui.QFileDialog.getOpenFileName(
1648            self, "Choose a weight file", "", "All files (*.*)",
1649            None, QtGui.QFileDialog.DontUseNativeDialog)
1650
1651        if datafile is None or str(datafile)=='':
1652            logging.info("No weight data chosen.")
1653            raise IOError
1654
1655        values = []
1656        weights = []
1657        def appendData(data_tuple):
1658            """
1659            Fish out floats from a tuple of strings
1660            """
1661            try:
1662                values.append(float(data_tuple[0]))
1663                weights.append(float(data_tuple[1]))
1664            except (ValueError, IndexError):
1665                # just pass through if line with bad data
1666                return
1667
1668        with open(datafile, 'r') as column_file:
1669            column_data = [line.rstrip().split() for line in column_file.readlines()]
1670            [appendData(line) for line in column_data]
1671
1672        # If everything went well - update the sasmodel values
1673        self.disp_model = POLYDISPERSITY_MODELS['array']()
1674        self.disp_model.set_weights(np.array(values), np.array(weights))
1675        # + update the cell with filename
1676        fname = os.path.basename(str(datafile))
1677        fname_index = self._poly_model.index(row_index, self.lstPoly.itemDelegate().poly_filename)
1678        self._poly_model.setData(fname_index, QtCore.QVariant(fname))
1679
1680    def setMagneticModel(self):
1681        """
1682        Set magnetism values on model
1683        """
1684        if not self.model_parameters:
1685            return
1686        self._magnet_model.clear()
1687        [self.addCheckedMagneticListToModel(param, self._magnet_model) for param in \
1688            self.model_parameters.call_parameters if param.type == 'magnetic']
1689        FittingUtilities.addHeadersToModel(self._magnet_model)
1690
1691    def shellNamesList(self):
1692        """
1693        Returns list of names of all multi-shell parameters
1694        E.g. for sld[n], radius[n], n=1..3 it will return
1695        [sld1, sld2, sld3, radius1, radius2, radius3]
1696        """
1697        multi_names = [p.name[:p.name.index('[')] for p in self.model_parameters.iq_parameters if '[' in p.name]
1698        top_index = self.kernel_module.multiplicity_info.number
1699        shell_names = []
1700        for i in xrange(1, top_index+1):
1701            for name in multi_names:
1702                shell_names.append(name+str(i))
1703        return shell_names
1704
1705    def addCheckedMagneticListToModel(self, param, model):
1706        """
1707        Wrapper for model update with a subset of magnetic parameters
1708        """
1709        if param.name[param.name.index(':')+1:] in self.shell_names:
1710            # check if two-digit shell number
1711            try:
1712                shell_index = int(param.name[-2:])
1713            except ValueError:
1714                shell_index = int(param.name[-1:])
1715
1716            if shell_index > self.current_shell_displayed:
1717                return
1718
1719        checked_list = [param.name,
1720                        str(param.default),
1721                        str(param.limits[0]),
1722                        str(param.limits[1]),
1723                        param.units]
1724
1725        FittingUtilities.addCheckedListToModel(model, checked_list)
1726
1727    def enableStructureFactorControl(self, structure_factor):
1728        """
1729        Add structure factors to the list of parameters
1730        """
1731        if self.kernel_module.is_form_factor or structure_factor == 'None':
1732            self.enableStructureCombo()
1733        else:
1734            self.disableStructureCombo()
1735
1736    def addExtraShells(self):
1737        """
1738        Add a combobox for multiple shell display
1739        """
1740        param_name, param_length = FittingUtilities.getMultiplicity(self.model_parameters)
1741
1742        if param_length == 0:
1743            return
1744
1745        # cell 1: variable name
1746        item1 = QtGui.QStandardItem(param_name)
1747
1748        func = QtGui.QComboBox()
1749        # Available range of shells displayed in the combobox
1750        func.addItems([str(i) for i in xrange(param_length+1)])
1751
1752        # Respond to index change
1753        func.currentIndexChanged.connect(self.modifyShellsInList)
1754
1755        # cell 2: combobox
1756        item2 = QtGui.QStandardItem()
1757        self._model_model.appendRow([item1, item2])
1758
1759        # Beautify the row:  span columns 2-4
1760        shell_row = self._model_model.rowCount()
1761        shell_index = self._model_model.index(shell_row-1, 1)
1762
1763        self.lstParams.setIndexWidget(shell_index, func)
1764        self._last_model_row = self._model_model.rowCount()
1765
1766        # Set the index to the state-kept value
1767        func.setCurrentIndex(self.current_shell_displayed
1768                             if self.current_shell_displayed < func.count() else 0)
1769
1770    def modifyShellsInList(self, index):
1771        """
1772        Add/remove additional multishell parameters
1773        """
1774        # Find row location of the combobox
1775        last_row = self._last_model_row
1776        remove_rows = self._model_model.rowCount() - last_row
1777
1778        if remove_rows > 1:
1779            self._model_model.removeRows(last_row, remove_rows)
1780
1781        FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index)
1782        self.current_shell_displayed = index
1783
1784        # Update relevant models
1785        self.setPolyModel()
1786        self.setMagneticModel()
1787
1788    def readFitPage(self, fp):
1789        """
1790        Read in state from a fitpage object and update GUI
1791        """
1792        assert isinstance(fp, FitPage)
1793        # Main tab info
1794        self.logic.data.filename = fp.filename
1795        self.data_is_loaded = fp.data_is_loaded
1796        self.chkPolydispersity.setCheckState(fp.is_polydisperse)
1797        self.chkMagnetism.setCheckState(fp.is_magnetic)
1798        self.chk2DView.setCheckState(fp.is2D)
1799
1800        # Update the comboboxes
1801        self.cbCategory.setCurrentIndex(self.cbCategory.findText(fp.current_category))
1802        self.cbModel.setCurrentIndex(self.cbModel.findText(fp.current_model))
1803        if fp.current_factor:
1804            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(fp.current_factor))
1805
1806        self.chi2 = fp.chi2
1807
1808        # Options tab
1809        self.q_range_min = fp.fit_options[fp.MIN_RANGE]
1810        self.q_range_max = fp.fit_options[fp.MAX_RANGE]
1811        self.npts = fp.fit_options[fp.NPTS]
1812        self.log_points = fp.fit_options[fp.LOG_POINTS]
1813        self.weighting = fp.fit_options[fp.WEIGHTING]
1814
1815        # Models
1816        self._model_model = fp.model_model
1817        self._poly_model = fp.poly_model
1818        self._magnet_model = fp.magnetism_model
1819
1820        # Resolution tab
1821        smearing = fp.smearing_options[fp.SMEARING_OPTION]
1822        accuracy = fp.smearing_options[fp.SMEARING_ACCURACY]
1823        smearing_min = fp.smearing_options[fp.SMEARING_MIN]
1824        smearing_max = fp.smearing_options[fp.SMEARING_MAX]
1825        self.smearing_widget.setState(smearing, accuracy, smearing_min, smearing_max)
1826
1827        # TODO: add polidyspersity and magnetism
1828
1829    def saveToFitPage(self, fp):
1830        """
1831        Write current state to the given fitpage
1832        """
1833        assert isinstance(fp, FitPage)
1834
1835        # Main tab info
1836        fp.filename = self.logic.data.filename
1837        fp.data_is_loaded = self.data_is_loaded
1838        fp.is_polydisperse = self.chkPolydispersity.isChecked()
1839        fp.is_magnetic = self.chkMagnetism.isChecked()
1840        fp.is2D = self.chk2DView.isChecked()
1841        fp.data = self.data
1842
1843        # Use current models - they contain all the required parameters
1844        fp.model_model = self._model_model
1845        fp.poly_model = self._poly_model
1846        fp.magnetism_model = self._magnet_model
1847
1848        if self.cbCategory.currentIndex() != 0:
1849            fp.current_category = str(self.cbCategory.currentText())
1850            fp.current_model = str(self.cbModel.currentText())
1851
1852        if self.cbStructureFactor.isEnabled() and self.cbStructureFactor.currentIndex() != 0:
1853            fp.current_factor = str(self.cbStructureFactor.currentText())
1854        else:
1855            fp.current_factor = ''
1856
1857        fp.chi2 = self.chi2
1858        fp.parameters_to_fit = self.parameters_to_fit
1859        fp.kernel_module = self.kernel_module
1860
1861        # Algorithm options
1862        # fp.algorithm = self.parent.fit_options.selected_id
1863
1864        # Options tab
1865        fp.fit_options[fp.MIN_RANGE] = self.q_range_min
1866        fp.fit_options[fp.MAX_RANGE] = self.q_range_max
1867        fp.fit_options[fp.NPTS] = self.npts
1868        #fp.fit_options[fp.NPTS_FIT] = self.npts_fit
1869        fp.fit_options[fp.LOG_POINTS] = self.log_points
1870        fp.fit_options[fp.WEIGHTING] = self.weighting
1871
1872        # Resolution tab
1873        smearing, accuracy, smearing_min, smearing_max = self.smearing_widget.state()
1874        fp.smearing_options[fp.SMEARING_OPTION] = smearing
1875        fp.smearing_options[fp.SMEARING_ACCURACY] = accuracy
1876        fp.smearing_options[fp.SMEARING_MIN] = smearing_min
1877        fp.smearing_options[fp.SMEARING_MAX] = smearing_max
1878
1879        # TODO: add polidyspersity and magnetism
1880
1881
1882    def updateUndo(self):
1883        """
1884        Create a new state page and add it to the stack
1885        """
1886        if self.undo_supported:
1887            self.pushFitPage(self.currentState())
1888
1889    def currentState(self):
1890        """
1891        Return fit page with current state
1892        """
1893        new_page = FitPage()
1894        self.saveToFitPage(new_page)
1895
1896        return new_page
1897
1898    def pushFitPage(self, new_page):
1899        """
1900        Add a new fit page object with current state
1901        """
1902        self.page_stack.append(new_page)
1903
1904    def popFitPage(self):
1905        """
1906        Remove top fit page from stack
1907        """
1908        if self.page_stack:
1909            self.page_stack.pop()
1910
Note: See TracBrowser for help on using the repository browser.